Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for cross-compilation with Nix #44

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open

Add support for cross-compilation with Nix #44

wants to merge 10 commits into from

Conversation

georgefst
Copy link
Owner

@georgefst georgefst commented Aug 12, 2022

The aim here is to build fully statically-linked binaries for Linux and Windows. This will make distribution easier. I particularly have in mind Steam games which distribute Monpad (EDIT: this is a lot less important now with dump-html mode - Ewephoria uses that and doesn't ship Monpad at all).

Various potentially-useful links about building static Haskell binaries:

We should also look at the setups of projects that do this (GHCup, HLS, Dhall...). Also note that we can't legally link GMP statically. Fortunately support for native Haskell bignums was recently added to Haskell.nix.

Some of the commits here are hacks (partly to get Ewephoria) out of the door, and shouldn't be merged. I'll probably make a new branch once the main hurdles have been overcome.

@@ -0,0 +1,25 @@
{
description = "monpad";
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had hoped to make this totally generic (remove ./haskell directory specifier, release flag etc.), and use it across other projects. Build.hs would then just pull it down when necessary from some repository (with an integrity check). But it seems that flake files have to be checked in, so that's a no-go.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, once we upload to Hackage (#20), the package will be picked up by Haskell.nix, all assets will be in the sdist, and we won't need to do anything special in Nix at all. Although perhaps it would be useful to retain the ability to deploy binaries without a corresponding Hackage version?

src = ./haskell;
compiler-nix-name = "ghc924";
shell.tools = { cabal = { }; };
shell = { inherit crossPlatforms; };
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to support nix build, rather than just *-cabal within nix develop (more on that elsewhere), we need to add configureArgs = "-frelease"; here if we don't want the built monpad to rely on finding the rsc folder. This is annoying because:

  • It's duplication between this file and the Shake build script.
  • It doesn't work! Presumably this is something to do with the asset files not being tracked by Nix. But I thought that Haskell.nix would handle that for us. I'm sure I even read somewhere about it having special support specifically for file-embed. nix build .#monpad:exe:monpad fails with:
    src/Embed.hs:32:27: error:
        • Exception when trying to run compile-time code:
            rsc/common.css: withBinaryFile: does not exist (No such file or direc>
          Code: (embedFile $ "rsc" </> "common.css")
        • In the untyped splice: $(embedFile $ "rsc" </> "common.css")
       |
    32 | commonCSS () = GET_FILE("rsc" </> "common.css")
       |                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    

Copy link
Owner Author

@georgefst georgefst Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • It doesn't work!

It does now! I think what changed is that I'm now working from a clean checkout. Somehow a dirty git working dir gets in the way. EDIT: got too excited - as stated above this currently builds a version with no embedded files

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hacked a solution in 814b4cc, but I should probably make those dependencies tracked by Nix, or something. I also have a no-symlinks stash for Build.hs modifications (although I'm not sure that's the best approach either, given assets need to be known to Git and I don't want duplicates).


If we're on x86-64 Linux, we can use `nix develop` to enter a shell from which we can cross-compile:
```sh
~/.ghcup/bin/runghc Build.hs --target=x86_64-w64-mingw32
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weirdly, we have to remove the Windows target from crossPlatforms in order for plain ~/.ghcup/bin/runghc Build.hs to work, i.e. to build for the host platform. This seems like a Haskell.nix bug. It's not a huge issue though, as we don't have much reason to use Nix at all for such a build. It would be nice to get cached dependencies though.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command actually fails quickly for TH reasons. It may be salvageable, but the fact it doesn't "just work" seems a limitation of the shell-based approach, as opposed to nix build.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just use nix build .#x86_64-w64-mingw32:monpad:exe:monpad, we would need to add some Nix config for compiling the non-Haskell parts of the project.

Perhaps it could just shell out to runghc Build.hs assets.

If we're on x86-64 Linux, we can use `nix develop` to enter a shell from which we can cross-compile:
```sh
~/.ghcup/bin/runghc Build.hs --target=x86_64-w64-mingw32
~/.ghcup/bin/runghc Build.hs --target=x86_64-unknown-linux-musl
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but isn't actually useful without passing some extra flags, since ldd shows just as many dependencies as the GCC version.

Again, this is a drawback of the shell-based approach.

Copy link
Owner Author

@georgefst georgefst Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand nix build .#x86_64-unknown-linux-musl:monpad:exe:monpad fails building libevdev, whereas interestingly the shell+cabal approach does not.

I assume the issue is getting hold of a statically-linked libevdev. I've asked about this on r/haskell.

If this is a real sticking point, we could somehow (separate branch, or Nix-based override) compile without --system-device support for now. It isn't actually necessary for my immediate use case. It's enough (though hacky) to replace the fields under os(linux) with just hs-source-dirs: windows.

It's possible that my Haskell evdev library is in some way not totally Musl compatible. I'd hope it would be, but it's untested. I recall the evdev-rs needing changes to explicitly support it. Otherwise maybe Haskell.nix just needs to be told about the native dependency.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have static musl builds working now, after providing a static libevdev. cabal build doesn't produce a static exe because the magic is in nix build somewhere. Another killer for nix develop+cabal approach I originally desired.

~/.ghcup/bin/runghc Build.hs --target=x86_64-unknown-linux-musl
~/.ghcup/bin/runghc Build.hs --target=aarch64-unknown-linux-gnu
```

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do with a section on deploying the resulting artefacts.

Copy link
Owner Author

@georgefst georgefst Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'll manage to fully statically link everything, meaning we'll just have a single executable file to deploy.

If we are stuck with shared libraries, we'll need commands like:

  • TARGET_DIR=... sh -c 'rsync result/ $TARGET_DIR/ -a --copy-links && rsync dist/rsc/ $TARGET_DIR/bin/rsc/ -a --copy-links'
  • TARGET_NAME=... sh -c 'nix copy --to ssh://$TARGET_NAME .#aarch64-unknown-linux-gnu:monpad:exe:monpad && scp ./result/bin/monpad $TARGET_NAME:tmp/monpad-cross-nix'

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now have static Linux builds, but Windows needs a load of DLLs (weirdly this is an inversion of the usual non-Nix state of things). Can we compile statically with MinGW?

flake.nix Outdated Show resolved Hide resolved
```sh
~/.ghcup/bin/runghc Build.hs --target=x86_64-w64-mingw32
~/.ghcup/bin/runghc Build.hs --target=x86_64-unknown-linux-musl
~/.ghcup/bin/runghc Build.hs --target=aarch64-unknown-linux-gnu
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed aarch64-multiplatform from crossPlatforms because this doesn't actually work. It causes mysterious evaluation-time failures (nix develop or nix build).

~/.ghcup/bin/runghc Build.hs --target=x86_64-w64-mingw32
~/.ghcup/bin/runghc Build.hs --target=x86_64-unknown-linux-musl
~/.ghcup/bin/runghc Build.hs --target=aarch64-unknown-linux-gnu
```
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the system GHC here is pretty odd. Can we just use cabal run (or even ./Build.hs)?

src = ./haskell;
compiler-nix-name = "ghc924";
shell.tools = { cabal = { }; };
shell = { inherit crossPlatforms; };
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This crossPlatforms is only necessary for nix build, which we were hoping not to use (the other being for nix develop, which gives us access to the Shake build script).

But right now, that's working and the other isn't.

haskellNix.overlay
(final: prev: prev.lib.optionalAttrs prev.stdenv.hostPlatform.isMusl {
libevdev = prev.libevdev.overrideAttrs (_: { dontDisableStatic = true; });
})
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should upstream this patch somehow, as initially suggested on Reddit.

@georgefst
Copy link
Owner Author

This looks interesting: https://gitlab.com/macaroni.dev/macaroni.nix.

It turns out files under `extra-source-files` are not found at compile time (`file-embed`) unless they're in `rsc` (and not as symlinks), and known to Git (staging makes a difference, which confused me for a while, e.g. in georgefst/hello-hs@fa41589).
This is primarily so that we can remove a dependency on the GPL-licensed libgmp. See https://github.com/input-output-hk/haskell.nix/pull/1784/commits.
@georgefst
Copy link
Owner Author

georgefst commented Dec 20, 2022

The commit "Work around Musl getAddrInfo issue" may be misplaced. Even on a native build on master, getAddrInfo returns Nothing on my main Linux machine, since getAddrInfo isn't returning any useful addresses (EDIT: nope - sometimes it does work, maybe when I'm connected directly to main router instead of an extender). I'm pretty sure this has broken in the past few months, and seems likely to be down to the OS rather than Haskell libraries or anything...

(prompted by seeing someone else using very similar code, and being curious about the slight differences)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant