Auto-recompiling a crystal project as I work

On various crystal projects when I’m developing, I want something akin to live-reload. I recently found a solution for this which has really changed my workflows for the better. Maybe it’ll save you some time too.

I came across a tool called watchexec which is a general purpose file watcher and command runner. It’s invocation is trivial, and it installs with brew easily.

I added this to my project/script/worker script:

#!/usr/bin/env bash

# brew install watchexec
watchexec \
  --filter '*.cr' \
  -r shards run worker

Then I can run script/worker to boot the worker and it’ll automatically recompile and run the process every time I save a file. When combined with save-on-focus-lost in my editor, this is a great feature.

I did the same thing with script/web which runs a kemal project, and then run both at the same time with Overmind and a Procfile.dev:

web: script/web
worker: script/worker

overmind s -f Procfile.dev runs both, and both will independently re-complie and re-run automatically.

Hope this helps someone!

5 Likes

I been using this for a while and it really is pretty great. I used to use nodemon - npm, but really like not having a dependency on node. One additional tip I’ll share is you can create a shell alias with it:

$ alias wcr
alias wcr='watchexec --signal=TERM --watch=src/ --emit-events-to=none --clear -- crystal $1'

Then this’ll allow you to do stuff like:

$ wcr run test.cr
$ wcr spec

To quickly use it without having to create a script file for it; which is especially nice for one-off test.cr files, or when working with specs. The --clear option is also really nice so you only ever see the output from the latest execution.

1 Like

I’ve been using entr for some time:

find . -iname '*.cr' | entr crystal spec -v

find . -iname '*.cr' -o -iname '*.ecr' | entr -r crystal run src/server.cr

It just watches the files it gets listed on stdin, so whatever find (or ‘fd’ if you’re so inclined) can find.

But then I realized that 99% of the time I want to run something when I save a file in my editor, so I cut out the middleman and wrote a small package for Emacs. Has the added benefit of keeping the command output hidden until the command fails.

1 Like

I looked at entr, but the caveat it comes with is that it only runs the find command once, so when I add a file to the tree it doesn’t know to watch it. watchexec doesn’t suffer from this, and I assume that’s because it’s using FSevent or something similar.

I use my fork of watcher (crystal) to do this (e.g. recompile server / restart it). I also play a sound after the compilation succeeded (or an error sound when it failed):

Watcher.watch(PATTERN) { |files, _|
  WatcherHelpers.print_changes("APP/SRC", ROOT / "src", files)

  r = build_cli

  if r.success?
    Sounds.up
  else
    Sounds.error
  end
  # ...
}

# ...
module Sounds
  def self.up
    Process.new("aplay", ["./sounds/up.wav"])
  end

  def self.error
    Process.new("aplay", ["./sounds/error.wav"])
  end
end
2 Likes

I am using GitHub - f/guardian: Guardian watches over your files and run assigned tasks.

Nice to see more people using watchexec and Overmind! This is precisely the approach I took on my projects (I don’t have Crystal installed locally, only inside a container):

:drum:

(note that the example predates the shards run command as that was added in 2022 :wink:

2 Likes

TIL shards run exists!

1 Like

Crystal has sentry serve as this, although it not live update any more. i create my own fork, fix bugs, and add sounds support(play different wav file when build sucess/failed), i create PR since Aug 8, 2022, but never get response from maintainer, so i am planning move my own fork to crystal-china org and do a big refactor then living maintain it.

it work well on linux, but never test it on macOS, maybe you can try and give some feedback? e.g. can i know wheather cat success.wav | afplay correct play sound on macOS ?

I did use Sentry in the past for this. Ultimately the problem I ran into was that I didn’t actually want to use a tool that needed to be built or compiled or have a runtime (eg node’s nodemon). Some node projects are now shipping packaged binaries (eg tailwind) which would have been fine.

I would love if Crystal had better support for this, though I confess I haven’t actually looked into it in some time. If sentry was both still maintained and published binaries which I could easily install, it would be worth considering again… until then, I just need the simplest thing that works.

I didn’t know why original README of sentry why so complex, in fact, just git pull my fork, and crystal build --release, then copy sentry into PATH, e.g. /usr/local/bin, then entry the project root, run sentry is just work.

I will add git action for build pre-build binary for each tag.

1 Like