How to build for release?

I always feel confused about building with Crystal.
The build methods adopted in GitHub Actions are quite inconsistent.

Code search results (github.com)

What is the appropriate command to build a release version?

  1. Is shards build --production --release --static --no-debug sufficient?
  2. When should I use crystal build?
  3. How to build universal binary for Darwin with GitHub Actions?

There are some Crystal build options like --static, --release, --no-debug, -Dpreview_mt that are not displayed in the shards prompt, but they don’t cause any errors. Should I use them? When should I use them?

Reference
crystal-build
shards-build
github-actions
static_linking

2 Likes

First of all, shards build always delegate all it unknown options into Crystal.

Then, if not add --no-debug, it always include debug info, add this option will reduce file size significantly.

Add -Dpreview_mt only if you know what you want.

--static not always work for current version Compiler, in fact, AFAIK, it only work when build static binary from alpine docker container, for build a linux static binary or an universal binary for Darwin without use docker, you probably want to check Use zig cc as a alternative linker (draft version) - #2 by zw963

you should always use --release when release a binary.

For --production, as said by shards help,

--production                     same as `--frozen --without-development`
1 Like

Working with zig is quite an interesting idea.
Currently I have got an effective workflow that is able to build release versions adapted to all Tier 1 platforms, most Tier 2 platforms and some Tier 3 platforms.
It’s partially relying on qemu and I am glad to find more efficient methods.

1 Like

Nice to see Chinese people using Crystal to make products!

I’m not fighting alone~

1 Like

@Sunrise Here’s some explanation: ā€œto buildā€ means compiling and linking an executable program.

The shards build command makes sure dependencies are installed before building, then it calls crystal build. It’s a mere convenience, and not required at all. Basically it does:

$ shards check || shards install
$ crystal build <target.main> -o bin/<target> <args>

The --production and --frozen options are shards’ options. They tell Shards to not install the development dependencies, and to install the locked dependencies. See shards install --help for more details.

The crystal build command is the command that builds your program. There’s a plethora of arguments you can pass to customize the build. See crystal build --help for details.

For building a production version of your program, you’ll likely want --release, that enables both --single-module and -O3 that will enable the most aggressive optimizations. It also adds the :release compile time flag that may be used to differentiate a development build vs a release build.

By default we only enable debug lines sections, in order to have file:line information in backtraces (exceptions and debuggers). This can be disabled with --no-debug. On the opposite, you can enable full debug info with --debug to be able to inspect variables in debuggers. I recommend to use neither unless you need to debug, in which case --debug will be useful.

The --static option will compile a static binary, that doesn’t need any shared library to run (they’re embedded right into the executable). It can be useful but can be tricky.

The -Dpreview_mt compile time flag enables a multi-threaded environment (by default crystal programs are single threaded). It makes the stdlib thread safe, and allows to run fibers in parallel in multiple threads. That being said it’s still in preview, community shards aren’t necessarily thread safe, and it’s under heavy refactor in RFC 0002 to finally make it official.

To build a program for production, you can configure a target in shard.yml then call:

$ shards build --production --frozen <target> --release

Or decompose in two calls, which is less confusing:

$ shards install --production --frozen
$ crystal build src/myapp.cr -o bin/myapp --release

Sadly, I can’t help with macOS universal builds.

11 Likes

Thank you for the detailed explanation.

I finally found a way to create fat binary for macOS on any platform and that is konoui/lipo. I have added it to my workflow and it works as expected. :smiling_face_with_three_hearts:

1 Like

The most awesome answer I have ever seen!

We need add this answer into somewhere in official book!

ping @straight-shoota

@ysbaddaden , I’m not quite clear about this part, you said enable --debug for used in debuggers, so, debuggers in this case, reference tools like lldb, gdb, right?

Because i saw no different when print the file:line backtrace when build with default or with --debug, except a slight increase in file size,

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

 ╰─ $ ls -alh bin/{website,website_no_debug,website_debug}
Permissions Size User  Date Modified Name
.rwxr-xr-x   35M zw963 16 minutes    bin/website*
.rwxr-xr-x   37M zw963 25 minutes    bin/website_debug*
.rwxr-xr-x   33M zw963 16 minutes    bin/website_no_debug*

only --no-debug missing the file:line info in backtrace, so, I wonder when the --debug option is used?


One more question, thanks

what exactly --frozen used for? shouldn’t shards install install the precise version contained in the lock file? what I mean is, shards update is the process that upgrades shard.lock, right?

The most awesome answer I have ever seen!

We need add this answer into somewhere in official book!

@straight-shoota , @beta-ziliani , Isn’t my suggestion not good?

If there is a shard.lock file, --frozen will work, but in the case of a lib project (not an app), the option will fail when tested against a clean checkout of the project:

$ shards install --frozen
Missing shard.lock

$ echo $?
1

If you do shards install it will install the dependencies indicated in shard.yml and it will generate a .lock file, at which point shards build --frozen (or --production) will not complain about the missing lock file.

Also:

--production implies both --frozen and --without-development, and --release gets forwarded to crystal build to build a release executable.

So:

$ shards build --production <target> --release

Is:

  • Install dependencies if there weren’t installed yet
  • Fail if there is no lock file
  • When installing the dependencies, do not install the development ones
  • When building <target>, forward --release argument.

Complicated, but I hope it helps! :sweat_smile:

Cheers.

1 Like

I mean exactly what I said: --no-debug allows you to inspect variables in debuggers.

--no-debug allows you to inspect variables in debuggers.

Sorry, still not clear, what is the debugger stand for in this Crystal context? debugger when used in Interpreter? or tools like gdb/lldb?

I mean an actual debugger like gdb or WinDBG because we build an executable with --debug The interpreter doesn’t compile code, but interprets it, and thus already has access to all the memory space (it owns it).

1 Like