Cross-compiling Crystal applications - Part 2

Hello folks!

Thank you all for the great feedback on Part 1 of my quest on scratching my own cross-compilation need for Crystal.

This time, here is Part 2, which automates most of the manual steps covered in the first part and includes macOS cross-compilation support!

For those that wants something working, please see the TL;DR section of the article, or checkout the README of the crystal-xbuild-container repository, that contains simple instructions to build the image and cross-compile locally.

Thank you so much for all the shared feedback, please let me know what do you think about this update.

Cheers!

5 Likes

The downside is that Homebrew is a macOS-only application (written in Ruby) and attempting something like I did before with apk might be too complicated.

Homebrew is not macOS-only. It actually runs on Linux as well. Of course the binary repositories are differnt, so fetching the binaries for a different OS than the host might still be complicated (or not; I have no clue).

Note that while Windows support in Crystal is getting better, does not yet provide an stable playground to my expectations.

This might be going a bit off-topic, so feel free to answer in a new thread: Which stability expectations are you missing on Windows?

I tried to keep it simple as Homebrew is more known to folks on macOS than Linux (before was known as Linuxbrew). It also depends on glibc and provides its own version, which cannot be statically linked, making it hard to build standalone applications. This can pose problems when attempting to run it against systems with different versions of glibc.

This is a problem that I haven’t explored yet, and I might do if there is enough interest on getting portable executables that can work against glibc.

For now, I’m limiting my effort to what I currently use: musl, with its own limitations it has served me well :blush:

Definitely will, but again, are my personal expectations and may not align with what Crystal and today’s Windows community might be expecting.

Thank you for the comments!
:heart: :heart: :heart:

It’s a good idea to try to keep things simple :+1: The field is already big enough.
But statements should still be factually correct.

Yeah, that’s the main problem with dynamically linked libraries from homebrew.
Apparently glibc isn’t meant to be linked statically. So most peole just use musl or an other libc implementation for statically linked binaries (or no libc at all).

Indeed, I will update the language and a few other things that I noticed after-re-re-reading it again :grin:

Please keep them coming! :blush:

Cheers,

Hello.
I tried this on Ubuntu at home. It did generated a Mac executable (but I didn’t have a Mac at home so I couldn’t verify that it would work). Then something went wrong with the permissions and I could not delete the directory using sudo rm. So I logged out and logged in.
I think crystal-xbuild-container is probably for a one-time run on CI/CD runner, not for someone’s Ubuntu desktop. I don’t think it’s a big problem, but I’m reporting it just in case.

Ah! Good catch! Indeed the permission issue is that the container is running as root and not your regular user.

The parent container that I use to build this does support running as an specific user and corrects the permission issues.

Simply add: -u $(id -u):$(id -g) to docker run command and it should run as your current user/group and generate a file owned by your current user.

I will update the repository and the instructions.

Thank you for giving it a test and report it back!
:heart: :heart: :heart:

1 Like

Hi, for guys who don’t want mess up with docker, i still maintain a fork of luislavena(this post author)'s magic-haversack repo, because he archive it, so, i move my fork it into here for long-term maintain it.

All honors belong to the original author luislavena.

Dependencies

If you are use linux, all dependencies is almost available, except zig compiler.

  • BASH (version > 4.0)
  • sed
  • zig compiler
  • Ruby (only if you want download the libraries by yourself, but i will upload those libraries as assets in release page later)

How to use it.

  1. git clone GitHub - crystal-china/magic-haversack: Facilitate Crystal cross-compilation
  2. Run bundle install then rake fetch:all, You can skip this step if you download libraries from github release page instead, and extract it into PROJECT_ROOT/lib, as following:
 ╰─ $ tree -L1 lib
lib
├── aarch64-linux-musl
├── aarch64-monterey
├── x86_64-linux-musl
└── x86_64-monterey
  1. Add PROJECT_ROOT/bin into $PATH, then you can use sb script for cross build a Crystal program.

  2. Entering the Crystal project you want to execute the build on, I use following command for built an AMD64 static binary which can copy into and running it on any linux host.

$: sb --cross-compile --target=x86_64-linux-musl --static --no-debug --link-flags=-s --release

Check following example for cross build a binary for x86_64-linux-musl, aarch-linux-musl,x86_64-darwin and aarch-darwin on my linux host.

 ╰─ $ sb --cross-compile --target=x86_64-linux-musl --static
zig cc -target x86_64-linux-musl bin/college.o -o bin/college  -rdynamic -static -L/home/zw963/Crystal/crystal-china/magic-haversack/lib/x86_64-linux-musl -lgmp -lyaml -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lpcre2-8 -lgc -lpthread -ldl -levent -lunwind

 ╰─ $ file bin/college
bin/college: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), static-pie linked, with debug_info, not stripped

 ╰─ $ sb --cross-compile --target=aarch64-linux-musl --static
zig cc -target aarch64-linux-musl bin/college.o -o bin/college  -rdynamic -static -L/home/zw963/Crystal/crystal-china/magic-haversack/lib/aarch64-linux-musl -lgmp -lyaml -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lpcre2-8 -lgc -lpthread -ldl -levent -lunwind

 ╰─ $ file bin/college
bin/college: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), static-pie linked, with debug_info, not stripped
 ╰─ $ sb --cross-compile --target=x86_64-darwin --static
zig cc -target x86_64-macos-none bin/college.o -o bin/college  -rdynamic -static -L/home/zw963/Crystal/crystal-china/magic-haversack/lib/x86_64-monterey -lgmp -lyaml -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lpcre2-8 -lgc -lpthread -ldl -levent -liconv -lunwind

  ╰─ $ file bin/college
bin/college: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|NO_REEXPORTED_DYLIBS|PIE|HAS_TLV_DESCRIPTORS>

 ╰─ $ sb --cross-compile --target=aarch64-darwin --static
zig cc -target aarch64-macos-none bin/college.o -o bin/college  -rdynamic -static -L/home/zw963/Crystal/crystal-china/magic-haversack/lib/aarch64-monterey -lgmp -lyaml -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lpcre2-8 -lgc -lpthread -ldl -levent -liconv -lunwind

 ╰─ $ file bin/college
bin/college: Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|NO_REEXPORTED_DYLIBS|PIE|HAS_TLV_DESCRIPTORS>

I have been using this workflow for a long time, and it work quite well, so i don’t want add unnecessary complexity for same purpose, don’t misunderstand me, i use docker quite well, but I use it only necessary.

4 Likes

Hi, i checked this blog today, for how to solve the issue about missing iconv, i don’t remember from where learn a alternative way.

  1. For the install macOS SDK section in the blog, I didn’t understand what was doing, but it feels so complicated anyway, in fact, you can install libiconv use homebrew as before instead, check my code here

  2. Add -Duse_libiconv when shards build with cross compile for darwin target, as I did here.

This solution easier than install macOS SDK, right?

Perhaps from PR#11876?

Using the Homebrew version of libiconv will result in something different from someone doing the native compilation on macOS.

I also know that you can download libiconv from Homebrew, as I wrote that original piece of code :wink:

Note that the macOS SDK is required to be able to link to the C library of macOS, not just for iconv (that comes with the OS)

Is not that I like to provide convoluted instructions for the sake of it and trying to make your life complicated, there is a reason behind it, which is based on experience.

What I’m sharing here is a distilled version based of the experience of cross-compiling apps and CLI for the past 3 years with Crystal and a few more years before doing that for Ruby on Windows for the RubyInstaller project.

While I’m currently bundling all this as a shippable container image, my final objective is to build something that can work with shards CLI natively, without containers and is transparent to the developer.

Cheers.

2 Likes

Yes, once again brought by you. :smile:

Apparently the macOS native iconv implementation is somewhat broken since 2023, so passing -Duse_libiconv is usually a good idea if your application relies on text encodings (unless we figure out a macOS-specific workaround)

1 Like

This is great information @HertzDevil, thanks for sharing!

I will update the post and the helper script to consider to handle this scenario.

Cheers.

Thank you folks, I pushed an update to crystal-xbuild-container repository that corrects some of the issues mentioned here.

Additionally, it will now use Homebrew’s libiconv automatically when targeting macOS.

The plus side is that this removes the needs for macOS SDK, at least for these libraries, but I keep it around as there are other things (Eg Security.framework, CoreFoundation, SecurityFoundation.framework, etc).

Cheers.

2 Likes

Hi, I checked the macOS SDK package to find libxml2.dylib, which is needed when linking app which add XML as dependency, but i only found a libxml2.tbd, i can install it from homebrew, but, what is the tbd file stand for? if add this SDK, do i still need install libxml2 from homebrew?

Thanks

See this answer from Apple on their developer forum:

For those who are curious, the .tbd files are new “text-based stub libraries”, that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size.

So the SDK ships the .tbd file with symbols and linking information for the .dylib that is part of the installation.

But perhaps, similar to the libiconv issue, the version of libxml that comes with macOS is older than the one from Homebrew? Knowing that libXML have severa CVE over the years, not sure if wouldn’t be safer to default to an updated Homebrew version instead.

To confirm this, I checked my macOS installation:

$ uname -s -m
Darwin arm64

$ which xml2-config
/usr/bin/xml2-config

$ /usr/bin/xml2-config --version
2.9.13

I assume that will be the version of libXML2 shipped with macOS.

Now, seems that Homebrew has a newer one: libxml2 — Homebrew Formulae

$ brew install libxml2

$ brew --prefix libxml2
/opt/homebrew/opt/libxml2

$ /opt/homebrew/opt/libxml2/bin/xml2-config --version
2.12.8

Seems newer than macOS one.


So in this case you have two options: you add libxml2 to the list of packages (both apk and homebrew downloader) or you link against the .tld from the SDK (the xbuild scripts adds the path to the lookup).

I will be adding the libraries to the container image just in case.

I hope that helps.

Cheers.

Yes, i done it before, it introduce the xz as dependencies too.

the .tbd files are new “text-based stub libraries”, that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size.

you link against the .tld from the SDK

Really impressive explain, a text based file can be linked to dynamically, I tried it on magic-haversack, replace libxml2.2.dylib and libxml2.dylib with it’s tbd variant, and it works when linking use zig cc, cool.

Knowing that libXML have severa CVE over the years, not sure if wouldn’t be safer to default to an updated Homebrew version instead.

If we don’t take this factor into consideration, I consider now using the SDK path as a zig cc lib search path is indeed a good choice.