diff --git a/.gitignore b/.gitignore index 66b36021..b9b9daf6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /radicle-cli/target +/build/artifacts diff --git a/build.rs b/build.rs index 620232c6..4105170c 100644 --- a/build.rs +++ b/build.rs @@ -4,57 +4,10 @@ use std::process::Command; fn main() -> Result<(), Box> { // Set a build-time `GIT_HEAD` env var which includes the commit id; // such that we can tell which code is running. - let hash = Command::new("git") - .arg("rev-parse") - .arg("--short") - .arg("HEAD") - .output() - .ok() - .and_then(|output| { - if output.status.success() { - String::from_utf8(output.stdout).ok() - } else { - None - } - }) - .unwrap_or(env::var("GIT_HEAD").unwrap_or("unknown".into())); - - let tags = Command::new("git") - .arg("tag") - .arg("--points-at") - .arg("HEAD") - .output() - .ok() - .and_then(|output| { - if output.status.success() { - String::from_utf8(output.stdout).ok() - } else { - None - } - }) - .unwrap_or_default(); - let tags = tags - .split_terminator('\n') - .filter_map(|s| s.strip_prefix('v')) - .collect::>(); - - if tags.len() > 1 { - return Err("More than one version tag found for commit {hash}: {tags:?}".into()); - } - // Used for `RADICLE_VERSION` env. - let version = if let Some(version) = tags.first() { - // There's a tag pointing at this `HEAD`. - // Eg. "1.0.43". - Some((*version).to_owned()) - } else { - // If `HEAD` doesn't have a tag pointing to it, this is a development version, - // so find the closest tag starting with `v` and append `-dev` to the version. - // Eg. "1.0.43-dev". + let hash = env::var("GIT_HEAD").unwrap_or_else(|_| { Command::new("git") - .arg("describe") - .arg("--match=v*") // Match tags starting with `v` - .arg("--candidates=1") // Only one result - .arg("--abbrev=0") // Don't add the commit short-hash to the tag name + .arg("rev-parse") + .arg("--short") .arg("HEAD") .output() .ok() @@ -65,26 +18,33 @@ fn main() -> Result<(), Box> { None } }) - .map(|last| format!("{}-dev", last.trim())) - } - // If there are no tags found, we'll just call this a pre-release. - .unwrap_or(String::from("pre-release")); + .unwrap_or("unknown".into()) + }); + + let version = if let Ok(version) = env::var("RADICLE_VERSION") { + version + } else { + "pre-release".to_owned() + }; // Set a build-time `GIT_COMMIT_TIME` env var which includes the commit time. - let commit_time = Command::new("git") - .arg("show") - .arg("--format=%ct") - .arg("HEAD") - .output() - .ok() - .and_then(|output| { - if output.status.success() { - String::from_utf8(output.stdout).ok() - } else { - None - } - }) - .unwrap_or(0.to_string()); + let commit_time = env::var("GIT_COMMIT_TIME").unwrap_or_else(|_| { + Command::new("git") + .arg("log") + .arg("-1") + .arg("--pretty=%ct") + .arg("HEAD") + .output() + .ok() + .and_then(|output| { + if output.status.success() { + String::from_utf8(output.stdout).ok() + } else { + None + } + }) + .unwrap_or(0.to_string()) + }); println!("cargo::rustc-env=RADICLE_VERSION={version}"); println!("cargo::rustc-env=GIT_COMMIT_TIME={commit_time}"); diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 00000000..e5b72559 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,108 @@ +# Builds release binaries for Radicle. +FROM rust:1.77.2-alpine3.19 as builder +LABEL maintainer="Radicle Team " +WORKDIR /src +COPY . . + +# Copy cargo configuration we're going to use to specify compiler options. +RUN mkdir -p .cargo && cp build/config.toml .cargo/config.toml +# Install dependencies. +RUN apk update && apk add --no-cache mold clang git musl-dev minisign curl xz asciidoctor +# Build man pages and strip metadata. Removes all comments, since they include +# non-reproducible information, such as version numbers. +RUN asciidoctor --doctype manpage --backend manpage --destination-dir . *.1.adoc && \ + find . -maxdepth 1 -type f -name '*.1' -exec sed -i '/^.\\\"/d' '{}' \; +# Add cargo targets. +RUN rustup target add \ + x86_64-unknown-linux-musl \ + aarch64-unknown-linux-musl \ + x86_64-apple-darwin \ + aarch64-apple-darwin + +# Linux x86_64 and aarch64 build. +ENV CC=clang C_INCLUDE_PATH=/usr/include/x86_64-linux-musl +RUN cargo build --locked --release \ + --target=x86_64-unknown-linux-musl \ + --target=aarch64-unknown-linux-musl \ + -p radicle-node \ + -p radicle-httpd \ + -p radicle-remote-helper \ + -p radicle-cli + +# Install dependencies for cross-compiling to macOS. +# We use Zig as the linker to perform the compilation from a Linux host. +# Zig is not yet available on Debian, so we download the official binary. +# Compilation is done via `cargo-zigbuild` which is a wrapper around `zig`. +RUN curl -sSf -o zig.tar.xz https://ziglang.org/builds/zig-linux-x86_64-0.12.0-dev.3678+130fb5cb0.tar.xz && \ + curl -sSf -o zig.tar.xz.minisig https://ziglang.org/builds/zig-linux-x86_64-0.12.0-dev.3678+130fb5cb0.tar.xz.minisig && \ + minisign -Vm zig.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U && \ + xz -d -c zig.tar.xz | tar -x && \ + mv zig-linux-x86_64-0.12.0-dev.3678+130fb5cb0/zig /usr/bin/zig && \ + mv zig-linux-x86_64-0.12.0-dev.3678+130fb5cb0/lib /usr/lib/zig && \ + cargo install cargo-zigbuild@0.18.3 + + +# Parts of the macOS SDK are required to build Radicle, we make these available +# here. So far only `CoreFoundation` and `Security` frameworks are needed. +RUN xz -d -c build/macos-sdk-11.3.tar.xz | tar -x +# This env var is used by `cargo-zigbuild` to find the SDK. +ENV SDKROOT /src/macos-sdk-11.3 + +# Darwin x86_64 and aarch64 build. +RUN cargo zigbuild --locked --release \ + --target=x86_64-apple-darwin \ + --target=aarch64-apple-darwin \ + -p radicle-node \ + -p radicle-httpd \ + -p radicle-remote-helper \ + -p radicle-cli + +# Now copy the files to a new image without all the intermediary artifacts to +# save some space. +FROM alpine:3.19 as packager +COPY --from=builder \ + /src/*.1 \ + /src/target/x86_64-unknown-linux-musl/release/rad \ + /src/target/x86_64-unknown-linux-musl/release/git-remote-rad \ + /src/target/x86_64-unknown-linux-musl/release/radicle-node \ + /src/target/x86_64-unknown-linux-musl/release/radicle-httpd \ + /builds/x86_64-unknown-linux-musl/ +COPY --from=builder \ + /src/*.1 \ + /src/target/aarch64-unknown-linux-musl/release/rad \ + /src/target/aarch64-unknown-linux-musl/release/git-remote-rad \ + /src/target/aarch64-unknown-linux-musl/release/radicle-node \ + /src/target/aarch64-unknown-linux-musl/release/radicle-httpd \ + /builds/aarch64-unknown-linux-musl/ +COPY --from=builder \ + /src/*.1 \ + /src/target/aarch64-apple-darwin/release/rad \ + /src/target/aarch64-apple-darwin/release/git-remote-rad \ + /src/target/aarch64-apple-darwin/release/radicle-node \ + /src/target/aarch64-apple-darwin/release/radicle-httpd \ + /builds/aarch64-apple-darwin/ +COPY --from=builder \ + /src/*.1 \ + /src/target/x86_64-apple-darwin/release/rad \ + /src/target/x86_64-apple-darwin/release/git-remote-rad \ + /src/target/x86_64-apple-darwin/release/radicle-node \ + /src/target/x86_64-apple-darwin/release/radicle-httpd \ + /builds/x86_64-apple-darwin/ + +# Create and compress reproducible archive. +WORKDIR /builds +RUN apk update && apk add --no-cache tar xz +RUN find * -maxdepth 0 -type d -exec mv '{}' "radicle-$RADICLE_VERSION-{}" \; && \ + find * -maxdepth 0 -type d -exec tar \ + --sort=name \ + --verbose \ + --mtime="@$GIT_COMMIT_TIME" \ + --owner=0 \ + --group=0 \ + --numeric-owner \ + --format=posix \ + --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \ + --mode='go+u,go-w' \ + --create --xz \ + --file="{}.tar.xz" \ + '{}' \; diff --git a/build/README.md b/build/README.md new file mode 100644 index 00000000..f2c6d96d --- /dev/null +++ b/build/README.md @@ -0,0 +1,70 @@ +# Builds + +Radicle uses a [reproducible build][rb] pipeline to make binary verification +easier and more secure. + +[rb]: https://reproducible-builds.org/ + +This build pipeline is designed to be run on an x86_64 machine running Linux. +The output is a set of `.tar.xz` archives containing binaries for the supported +platforms and signed by the user's Radicle key. + +These binaries are statically linked to be maximally portable, and designed to +be reproducible, byte for byte. + +To run the build, simply enter the following command from the repository root: + + build/build.sh + +This will build all targets and place the output in `build/artifacts` with +one sub-directory per build target. + +Note that it will use `git describe` to get a version number for the build. +You *must* have a commit tagged with a version in your history or the build +will fail, eg. `v1.0.0`. + +When the build completes, the SHA-256 checksums of the artifacts are output. +For a given Radicle version and source tree, the same set of checksums should +always be output, no matter where or when the build is run. If they do not +match, either the build pipeline has a bug, making it non-reproducible, or one +of the machines is compromised. + +Here's an example output for a development version of Radicle: + + b9aa75bba175e18e05df4f6b39ec097414bbf56ccdeb4a2229b557f8b8e05404 radicle-1.0.0-rc.4-3-gb299f3b5-aarch64-apple-darwin.tar.xz + c7070806bf2d17a8a0d3b329e4d57b1e544b7b82cb58e2863074d96348a2ab0d radicle-1.0.0-rc.4-3-gb299f3b5-aarch64-unknown-linux-musl.tar.xz + 1a8327854f16ea90491fb90e0c3291a63c4b2ab01742c8435faec7d370cacb79 radicle-1.0.0-rc.4-3-gb299f3b5-x86_64-apple-darwin.tar.xz + 709ac67541ff0c0c570ac22ab2de9f98320e0cc2cc9b67f1909c014a2bb5bd49 radicle-1.0.0-rc.4-3-gb299f3b5-x86_64-unknown-linux-musl.tar.xz + +A script is included in `build/checksums.sh` to output these checksums after +the artifacts are built. + +## Requirements + +The following software is required for the build: + + * `podman` + * `rad` (The Radicle CLI) + * `sha256sum` + +## macOS + +macOS binaries are not signed or notarized, so they have to be downloaded via +the CLI to avoid issues. A copy of a small subset of the Apple SDK is included +here to be able to cross-compile. + +## Podman + +We use `podman` to make the build reproducible on any machine by controlling +the build environment. We prefer `podman` to `docker` because it doesn't +require a background process to run and can be run without root access out of +the box. + +The first time you run `podman`, you may have to give yourself some extra UIDs +for `podman` to use, with: + + sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER + +Then update `podman` with: + + podman system migrate diff --git a/build/build.sh b/build/build.sh new file mode 100755 index 00000000..b0b97922 --- /dev/null +++ b/build/build.sh @@ -0,0 +1,102 @@ +#!/bin/sh +set -e + +main() { + # Use UTC time for everything. + export TZ=UTC0 + # Set minimal locale. + export LC_ALL=C + # Set source date. This is honored by `asciidoctor` and other tools. + export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) + + if ! command -v rad > /dev/null; then + echo "fatal: rad is not installed" ; exit 1 + fi + + if ! command -v podman > /dev/null; then + echo "fatal: podman is not installed" ; exit 1 + fi + + if ! command -v sha256sum > /dev/null; then + echo "fatal: sha256sum is not installed" ; exit 1 + fi + + rev="$(git rev-parse --short HEAD)" + tempdir="$(mktemp -d)" + gitarchive="$tempdir/heartwood-$rev.tar.gz" + keypath="$(rad path)/keys/radicle.pub" + + if ! version="$(git describe --match='v*' --candidates=1 2>/dev/null)"; then + echo "fatal: no version tag found by 'git describe'" ; exit 1 + fi + # Remove `v` prefix from version. + version=${version#v} + image=radicle-build-$version + + if [ ! -f "$keypath" ]; then + echo "fatal: no key found at $keypath" ; exit 1 + fi + # Authenticate user for signing + rad auth + + echo "Building Radicle $version.." + echo "Creating archive of repository at $rev in $gitarchive.." + git archive --format tar.gz -o "$gitarchive" HEAD + + echo "Building image ($image).." + podman --cgroup-manager=cgroupfs build \ + --env SOURCE_DATE_EPOCH \ + --env GIT_COMMIT_TIME=$SOURCE_DATE_EPOCH \ + --env GIT_HEAD=$rev \ + --env RADICLE_VERSION=$version \ + --arch amd64 --tag $image -f ./build/Dockerfile - < $gitarchive + + echo "Creating container (radicle-build-container).." + podman --cgroup-manager=cgroupfs create --replace --name radicle-build-container $image + + targets="\ + x86_64-unknown-linux-musl \ + aarch64-unknown-linux-musl \ + x86_64-apple-darwin \ + aarch64-apple-darwin" + + for target in $targets; do + outdir=build/artifacts/$target + + echo "Copying artifacts for $target.." + mkdir -p $outdir + rm -rf $outdir/* + + filename="radicle-$version-$target.tar.xz" + filepath="$outdir/$filename" + + # Copy archive to target folder. + podman cp radicle-build-container:/builds/$filename $outdir + + # Output SHA256 digest of archive. + checksum="$(cd $outdir && sha256sum $filename)" + echo "Checksum of $filepath is $(echo "$checksum" | cut -d' ' -f1)" + echo "$checksum" > $filepath.sha256 + + # Sign archive and verify archive. + rm -f $filepath.sig # Delete existing signature + ssh-keygen -Y sign -n file -f $keypath $filepath + ssh-keygen -Y check-novalidate -n file -s $filepath.sig < $filepath + done + + # Remove build artifacts that aren't needed anymore. + rm -f $gitarchive + podman rm radicle-build-container > /dev/null + podman rmi --ignore localhost/$image +} + +# Run build. +echo "Running build.." +main "$@" + +# Show artifact checksums. +echo +build/checksums.sh +echo + +echo "Build ran successfully." diff --git a/build/checksums.sh b/build/checksums.sh new file mode 100755 index 00000000..a0225cf5 --- /dev/null +++ b/build/checksums.sh @@ -0,0 +1,2 @@ +#!/bin/sh +find build/artifacts -type f -name '*.sha256' -exec cat {} + diff --git a/build/config.toml b/build/config.toml new file mode 100644 index 00000000..1e7a14a0 --- /dev/null +++ b/build/config.toml @@ -0,0 +1,19 @@ +[target.x86_64-unknown-linux-musl] +linker = "clang" +rustflags = [ + "-C", "link-arg=-fuse-ld=mold", + "-C", "link-arg=--target=x86_64-unknown-linux-musl", + "-C", "codegen-units=1", + "-C", "incremental=false", + "-C", "opt-level=3", +] + +[target.aarch64-unknown-linux-musl] +linker = "clang" +rustflags = [ + "-C", "link-arg=-fuse-ld=mold", + "-C", "link-arg=--target=aarch64-unknown-linux-musl", + "-C", "codegen-units=1", + "-C", "incremental=false", + "-C", "opt-level=3", +] diff --git a/build/macos-sdk-11.3.tar.xz b/build/macos-sdk-11.3.tar.xz new file mode 100644 index 00000000..02fad1f9 Binary files /dev/null and b/build/macos-sdk-11.3.tar.xz differ diff --git a/radicle/src/version.rs b/radicle/src/version.rs index 77df15ad..81534386 100644 --- a/radicle/src/version.rs +++ b/radicle/src/version.rs @@ -21,11 +21,8 @@ impl<'a> Version<'a> { .. } = self; - if version.ends_with("-dev") { - writeln!(w, "{name} {version}+{commit}")?; - } else { - writeln!(w, "{name} {version} ({commit})")?; - }; + writeln!(w, "{name} {version} ({commit})")?; + Ok(()) } @@ -52,17 +49,5 @@ mod test { .unwrap(); let res = std::str::from_utf8(&buffer).unwrap(); assert_eq!("rad 1.2.3 (28b341d)\n", res); - - let mut buffer = Vec::new(); - Version { - name: "rad", - version: "1.2.3-dev", - commit: "28b341d", - timestamp: "", - } - .write(&mut buffer) - .unwrap(); - let res = std::str::from_utf8(&buffer).unwrap(); - assert_eq!("rad 1.2.3-dev+28b341d\n", res); } } diff --git a/scripts/build-man-pages.sh b/scripts/build-man-pages.sh index 606f79a1..55bcc026 100755 --- a/scripts/build-man-pages.sh +++ b/scripts/build-man-pages.sh @@ -2,6 +2,30 @@ set -e +# Attempt to install `asciidoctor` on Debian, Arch Linux and MacOS. +install() { + os="$(uname)" + + case "$os" in + Linux) + if command -v apt-get >/dev/null 2>&1; then + # Debian + apt-get update + apt-get install -y asciidoctor + elif command -v pacman >/dev/null 2>&1; then + # Arch Linux + pacman -Sy --noconfirm asciidoctor + fi ;; + Darwin) # MacOS + if command -v brew >/dev/null 2>&1; then + brew install asciidoctor + fi ;; + *) + echo "fatal: unknown operating system: $os" + exit 1 ;; + esac +} + main() { if [ $# -lt 2 ]; then echo "usage: $0 ..." @@ -11,6 +35,11 @@ main() { outdir="$1" shift + if ! command -v asciidoctor >/dev/null 2>&1; then + echo "Installing 'asciidoctor'.." + install + fi + for input in "$@"; do echo "Building $input.." asciidoctor --doctype manpage --backend manpage --destination-dir "$outdir" "$input"