diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000000..0916b26707 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,61 @@ +on: + pull_request: + paths: + - 'nix/**' + - 'go.mod' + - 'go.sum' + - 'vendor/**' + - 'skywire.go' + - 'cmd/**' + - 'pkg/**' + - 'internal/**' + - '.github/workflows/nix.yml' + push: + branches: + - develop + paths: + - 'nix/**' + - 'go.mod' + - 'go.sum' + - 'vendor/**' + - 'skywire.go' + - 'cmd/**' + - 'pkg/**' + - 'internal/**' + - '.github/workflows/nix.yml' + +name: Nix + +jobs: + build-source: + name: Build skywire (static musl) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + accept-flake-config = true + + # Free shared Nix cache; speeds up consecutive runs from cold + # to warm by populating /nix/store from previous runs of this + # workflow (across branches/PRs). + - uses: DeterminateSystems/magic-nix-cache-action@main + + # Build the source-only target. skywire-bin is intentionally + # skipped here — it wants per-arch sha256s for upstream + # release tarballs; until those land in nix/flake.nix it + # would always fail. A separate workflow can validate it once + # release-time hashes are populated. + - name: nix build .#skywire + working-directory: nix + run: nix build .#skywire --print-build-logs + + - name: smoke-test the binary + working-directory: nix + run: | + ./result/bin/skywire --bv + ./result/bin/skywire-cli --help >/dev/null + ./result/bin/skywire-visor --help >/dev/null diff --git a/README.md b/README.md index 11063d9a7d..51d85c6489 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ Table of Contents - [Linux Packages](#linux-packages) - [Debian packages](#debian-packages) - [Arch Linux AUR packages](#arch-linux-aur-packages) + - [NixOS / Nix flake](#nixos--nix-flake) - [Docker](#docker) - [How to create a GitHub release](#how-to-create-a-github-release) - [Dependency Graph](#dependency-graph) @@ -1064,6 +1065,32 @@ Build the skywire Arch Linux package from git sources to the latest commits on t yay --mflags " -p git.PKGBUILD " -S skywire ``` +### NixOS / Nix flake + +Two derivations under [`nix/`](/nix/) — same flavors as the AUR +packages: `skywire` (source build, static-musl, mirrors +`make build-static`) and `skywire-bin` (the upstream release +tarball). + +From a checkout: +``` +cd nix +nix build .#skywire # source, static musl +nix build .#skywire-bin # prebuilt tarball +nix run .#skywire -- --bv +``` + +Or as a flake input from elsewhere: +```nix +inputs.skywire.url = "github:skycoin/skywire?dir=nix"; +# ... +environment.systemPackages = [ skywire.packages.${system}.skywire ]; +``` + +See [`nix/README.md`](/nix/README.md) for the per-arch hash-fill +flow on `skywire-bin`, the visor's `--apps-dir` integration, and +the static-binary sanity check. + ## Docker For docker-specific documentation, see: [DOCKER.md](/DOCKER.md) diff --git a/nix/README.md b/nix/README.md new file mode 100644 index 0000000000..99ed3ab0d5 --- /dev/null +++ b/nix/README.md @@ -0,0 +1,100 @@ +# Skywire — Nix packaging + +Two derivations, mirroring the AUR `skywire` and `skywire-bin` +packages: + +- **`skywire.nix`** — source build. Uses `pkgsStatic.buildGoModule` + (musl-based stdenv), with the same `-linkmode external -extldflags + "-static" -buildid=` ldflags as the upstream `make build-static` + recipe. Produces a fully-static binary identical in shape to the + release tarballs. + +- **`skywire-bin.nix`** — install the prebuilt release tarball from + GitHub releases. Faster build, trusts the upstream binary. + +Both produce the same layout: + + $out/bin/skywire # merged binary + $out/bin/skywire-cli # shim → skywire cli + $out/bin/skywire-visor # shim → skywire visor + $out/share/skywire/apps/ # shims for skychat, skysocks, vpn-*, etc. + +## Quick start + +From a checkout of this repo: + + cd nix + nix build .#skywire # source build (static musl) + nix build .#skywire-bin # download prebuilt + nix run .#skywire -- --bv + +Or as a flake input from somewhere else: + + { + inputs.skywire.url = "github:0pcom/skywire/nix/packaging"; + # … + outputs = { self, nixpkgs, skywire, ... }: { + # nixosConfigurations / systemPackages / etc. + environment.systemPackages = [ + skywire.packages.${system}.skywire + ]; + }; + } + +## First-time hashes + +`skywire-bin.nix` ships with `lib.fakeHash` placeholders for every +arch's release tarball. The first `nix build .#skywire-bin` will +fail and print the expected hash for the arch you're on; copy it +into the `hashes` attrset in `flake.nix` and rebuild. Repeat for +each arch you want to support. + +`skywire.nix` defaults to building from the **local working tree** +(handy for iterating on a branch). To build a tagged release from +upstream instead, override `rev` and `version` when calling it from +the flake (the parameter for an explicit src tree is `srcOverride`, +to dodge a `pkgs.src` callPackage auto-bind collision in nixpkgs): + + pkgs.callPackage ./skywire.nix { + rev = "v1.3.50"; + version = "1.3.50"; + } + +The first such build will fail with the expected `hash` for +`fetchFromGitHub`; copy it into the `hash = lib.fakeHash` line in +`skywire.nix`. + +## Visor app discovery + +The visor launches `skychat`, `skysocks`, `vpn-server`, etc. as +separate processes. Both packages install dispatcher shims under +`$out/share/skywire/apps/`. Point the visor at that directory: + + skywire visor --apps-dir ${skywire}/share/skywire/apps … + +or, in a config file, set `launcher.bin_path` to the same path. + +## NixOS module — TODO + +A `nixosModules.skywire` exposing `services.skywire.enable`, +service-tree toggles (sn / sd / tpd / dmsg / ar / rf), and the +autoconfig wiring is the natural follow-up. Not in this initial +drop. Once it lands, you'll be able to: + + services.skywire = { + enable = true; + package = pkgs.skywire; # or skywire-bin + services.tpd.enable = true; + # … + }; + +## Validating the build + +To sanity-check the source build is actually static: + + nix build .#skywire + file ./result/bin/skywire + # → ELF 64-bit LSB executable, x86-64, statically linked, … + +If `file` reports `dynamically linked`, the build's `postInstall` +guard already would have failed. diff --git a/nix/flake.nix b/nix/flake.nix new file mode 100644 index 0000000000..f9a86890a6 --- /dev/null +++ b/nix/flake.nix @@ -0,0 +1,98 @@ +{ + description = "Skywire packaged for Nix — source build (static musl) and prebuilt-binary variants"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + # Skywire ships without an explicit LICENSE file (the AUR + # PKGBUILD tags it "license-free"), so Nix's policy treats + # it as unfree by default and refuses to build it without an + # opt-in. Whitelist exactly skywire + skywire-bin here so + # `nix build` works on a stock Nix installation without + # exporting NIXPKGS_ALLOW_UNFREE=1 / --impure. + pkgs = import nixpkgs { + inherit system; + config.allowUnfreePredicate = pkg: + builtins.elem (nixpkgs.lib.getName pkg) [ + "skywire" + "skywire-bin" + ]; + }; + + # Pin the version once for both packages. Bump this when + # cutting a new release — the source build picks the matching + # git tag (or the working tree, the default), and the + # binary build expects upstream tarballs at v${version}. + version = "1.3.50"; + + skywire = pkgs.callPackage ./skywire.nix { + inherit version; + # Default: build from the local working tree (../..). To + # build a tagged release from upstream instead, override: + # + # nix build .#skywire \ + # --override-input src \ + # 'github:skycoin/skywire/v1.3.50' + }; + + skywire-bin = pkgs.callPackage ./skywire-bin.nix { + inherit version; + # Fill in real hashes here once the release is cut. First + # `nix build .#skywire-bin` will print the expected hash + # for whichever arch you're on; copy it in. + # + # hashes = { + # "linux-amd64" = "sha256-..."; + # "linux-arm64" = "sha256-..."; + # "linux-386" = "sha256-..."; + # "linux-armhf" = "sha256-..."; + # "linux-arm" = "sha256-..."; + # "linux-riscv64" = "sha256-..."; + # }; + }; + in { + packages = { + inherit skywire skywire-bin; + default = skywire; + }; + + # `nix run .#skywire-cli ...` etc. without typing the bin path. + apps = { + skywire = { + type = "app"; + program = "${skywire}/bin/skywire"; + }; + skywire-cli = { + type = "app"; + program = "${skywire}/bin/skywire-cli"; + }; + skywire-visor = { + type = "app"; + program = "${skywire}/bin/skywire-visor"; + }; + skywire-bin = { + type = "app"; + program = "${skywire-bin}/bin/skywire"; + }; + default = self.apps.${system}.skywire; + }; + + # `nix develop` drops you into a shell with the toolchain + # the source build needs (Go, musl, the standard CLI deps), + # mirroring `make build-static` requirements. + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + go + musl + pkg-config + gnumake + git + ]; + }; + }); +} diff --git a/nix/skywire-bin.nix b/nix/skywire-bin.nix new file mode 100644 index 0000000000..942f0a6eeb --- /dev/null +++ b/nix/skywire-bin.nix @@ -0,0 +1,148 @@ +{ + lib, + stdenvNoCC, + fetchurl, + runtimeShell, + version ? "1.3.50", + hashes ? null, +}: + +# skywire-bin — install the statically-linked release binary +# Skycoin publishes on GitHub. Mirrors the AUR `skywire-bin/PKGBUILD`: +# fetch tarball for the current arch, drop the `skywire` binary into +# $out/bin, and create the same wrapper shims. +# +# Release binaries are built static-musl upstream (same recipe as +# `skywire.nix`), so no autoPatchelfHook is needed — they run on +# any glibc/musl host. +# +# `hashes` is an attrset keyed by the `linux-` suffix used in +# the release filename: +# +# { +# "linux-amd64" = "sha256-..."; +# "linux-arm64" = "sha256-..."; +# "linux-386" = "sha256-..."; +# "linux-armhf" = "sha256-..."; +# "linux-arm" = "sha256-..."; +# "linux-riscv64" = "sha256-..."; +# } +# +# When omitted, defaults to `lib.fakeHash` for every arch — first +# real build will fail with the correct expected hash, fill them +# in. The flake at nix/flake.nix exposes a single `skywire-bin` +# package per system; this file is the per-system derivation. + +let + # Map nixpkgs `system` strings to the suffix Skycoin uses in + # release filenames. Returns null on systems with no published + # binary so the caller can short-circuit (we evaluate to a + # broken-package on those rather than failing eval). + archSuffixFor = system: + { + "x86_64-linux" = "linux-amd64"; + "aarch64-linux" = "linux-arm64"; + "i686-linux" = "linux-386"; + "armv7l-linux" = "linux-armhf"; + "armv6l-linux" = "linux-arm"; + "riscv64-linux" = "linux-riscv64"; + }.${system} or null; + + archSuffix = archSuffixFor stdenvNoCC.hostPlatform.system; + + effectiveHashes = + if hashes != null then hashes + else { + "linux-amd64" = lib.fakeHash; + "linux-arm64" = lib.fakeHash; + "linux-386" = lib.fakeHash; + "linux-armhf" = lib.fakeHash; + "linux-arm" = lib.fakeHash; + "linux-riscv64" = lib.fakeHash; + }; + + hash = + if archSuffix == null + then null + else effectiveHashes.${archSuffix} or lib.fakeHash; + + apps = [ + "skychat" + "skysocks" + "skysocks-client" + "vpn-server" + "vpn-client" + "skynet-srv" + "skynet-client" + ]; +in + +if archSuffix == null then + throw "skywire-bin: no upstream release binary published for ${stdenvNoCC.hostPlatform.system}" +else + +stdenvNoCC.mkDerivation { + pname = "skywire-bin"; + inherit version; + + src = fetchurl { + url = "https://github.com/skycoin/skywire/releases/download/v${version}/skywire-v${version}-${archSuffix}.tar.gz"; + inherit hash; + }; + + # Tarball is a single `skywire` binary at the top level — no + # subdir, so unpack stripping zero leading path components. + sourceRoot = "."; + + dontConfigure = true; + dontBuild = true; + + # Static-musl binary — patchelf has nothing to patch, and trying + # would fail. Skip stripping too in case Skycoin shipped any + # symbol info we'd want for debugging. + dontPatchELF = true; + dontStrip = true; + + installPhase = '' + runHook preInstall + + install -Dm755 skywire $out/bin/skywire + + cat > $out/bin/skywire-cli < $out/bin/skywire-visor < $out/share/skywire/apps/${app} < — shims for each native app +# (skywire app ) so the +# visor can launch them with the +# legacy "external apps directory" +# layout. +# +# The visor's BinPath defaults to "./apps" relative to its working +# directory; point it at $out/share/skywire/apps with +# visor.binPath = "${pkgs.skywire}/share/skywire/apps" +# in the NixOS module (or `--apps-dir` on the command line). + +let + selfSrc = + if srcOverride != null + then srcOverride + else if rev != null + then + fetchFromGitHub { + owner = "skycoin"; + repo = "skywire"; + inherit rev; + # Replace with `nix-prefetch-github skycoin skywire --rev `. + hash = lib.fakeHash; + } + else + # Default: build from the local working tree so `nix build` + # in this checkout exercises the current branch's code. + ../.; + + # Apps that the visor launches as separate binaries; the merged + # `skywire` exposes each as `skywire app `. We install + # shim scripts under $out/share/skywire/apps/ so the visor + # can find them by the legacy filename. + apps = [ + "skychat" + "skysocks" + "skysocks-client" + "vpn-server" + "vpn-client" + "skynet-srv" + "skynet-client" + ]; +in +pkgsStatic.buildGoModule { + pname = "skywire"; + inherit version; + src = selfSrc; + + # The repo vendors all dependencies (`vendor/`); skip the module + # download phase and use them directly. If vendor/ is removed + # upstream, replace with a real `vendorHash`. + vendorHash = null; + + # The repo root's `skywire.go` is the merged-binary entrypoint + # (skywire + skycoin subtrees); same target the upstream + # Makefile's `build-static` recipe builds with `go build .`. + # `cmd/skywire/` is a leaner skywire-only package not used here. + subPackages = [ "." ]; + + # Static-link via musl + the same -extldflags '-static' the + # upstream Makefile uses. CGO_ENABLED is forced on because the + # external linker only fires when cgo is in play. + env.CGO_ENABLED = "1"; + + # buildGoModule already passes `-buildid=` by default; supplying + # it here would just produce a warning. Keep the rest: -s/-w + # strip the symbol table, -linkmode external + -extldflags + # -static is what musl-gcc-driven static linking needs, and the + # -X injections populate the same buildinfo vars the upstream + # Makefile sets. + ldflags = [ + "-s" + "-w" + "-linkmode" "external" + "-extldflags" "-static" + "-X" "github.com/skycoin/skywire/pkg/buildinfo.version=v${version}" + "-X" "github.com/skycoin/skywire/pkg/visor.BuildTag=nix_static" + ]; + + # `go build .` produces a binary named after the repo dir + # (`skywire`) — keep it; the AUR shims expect `skywire` too. + postInstall = '' + # Sanity-check the binary actually came out static. Static + # ELFs lack a PT_INTERP segment; readelf reports "Requesting + # program interpreter" only on dynamic executables. Anything + # dynamic here would silently break portability of the closure. + if ${pkgsStatic.buildPackages.binutils-unwrapped}/bin/readelf -l $out/bin/skywire \ + | grep -q 'Requesting program interpreter'; then + echo "skywire binary is dynamically linked — static build failed" >&2 + exit 1 + fi + + # CLI / visor wrapper shims (mirrors AUR layout). + cat > $out/bin/skywire-cli < $out/bin/skywire-visor <`. + install -d $out/share/skywire/apps + '' + lib.concatMapStringsSep "\n" (app: '' + cat > $out/share/skywire/apps/${app} <