Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,15 @@ blocks:
commands:
- "just lint-light --show-diff-on-failure --all"
- "just lint-heavy"
- name: "Tag"
dependencies: []
run:
when: "tag =~ '^v[[:digit:]]+$'"
task:
jobs:
- name: "Build & stamp tagged Opsqueue binary"
commands:
# Build the binary with the release tag stamped in. The Rust compilation is
# pulled from cachix (built by the regular Build block); only the fast
# binary-patching step runs here.
- "nix-store -qR --include-outputs $(nix-store -qd $(just nix-build-bin-tagged \"$SEMAPHORE_GIT_TAG_NAME\")) | grep -v '\\.drv$' | cachix push channable"
5 changes: 5 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,10 @@ nix-build-bin: (_nix-build "opsqueue")
[group('nix')]
nix-build-python: (_nix-build "python.pkgs.opsqueue_python")

# Build Nix-derivation of binary with a release tag stamped in without recompiling (release profile)
[group('nix')]
nix-build-bin-tagged TAG:
nix build --expr "(import ./nix/nixpkgs-pinned.nix {}).opsqueue.withTag \"{{TAG}}\"" --print-out-paths --print-build-logs --no-link --option sandbox true

_nix-build +TARGETS:
nix build --file nix/nixpkgs-pinned.nix --print-out-paths --print-build-logs --no-link --option sandbox true {{TARGETS}}
41 changes: 30 additions & 11 deletions opsqueue/opsqueue.nix
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,35 @@ let
# serially, this is the slowest part on CI. We have other CI linting steps, i.e. `cargo clippy`,
# that report these errors earlier.
cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { cargoCheckCommand = "true"; });
in
craneLib.buildPackage (
commonArgs
// {
inherit cargoArtifacts;

# Needed for the SQLx macros:
env = {
DATABASE_URL = "sqlite:///build/opsqueue/opsqueue/opsqueue_example_database_schema.db";
};
opsqueuePkg = craneLib.buildPackage (
commonArgs
// {
inherit cargoArtifacts;

}
)
# Needed for the SQLx macros:
env = {
DATABASE_URL = "sqlite:///build/opsqueue/opsqueue/opsqueue_example_database_schema.db";
};
}
);
in
opsqueuePkg.overrideAttrs (old: {
passthru = (old.passthru or { }) // {
# Stamp a release tag into the pre-compiled binary without recompiling Rust.
#
# Usage (in a downstream Nix derivation):
# pkgs.opsqueue.withTag "v1.2.3"
#
# The `pkgs.opsqueue` derivation is built once and cached; only the fast
# binary-patching step reruns when the tag changes.
withTag =
tag:
pkgs.runCommand "${opsqueuePkg.name}-tagged" { nativeBuildInputs = [ pkgs.python3 ]; } ''
mkdir -p $out/bin
cp ${opsqueuePkg}/bin/opsqueue $out/bin/opsqueue
chmod +w $out/bin/opsqueue
python3 ${./stamp-tag.py} "$out/bin/opsqueue" ${lib.escapeShellArg tag}
'';
};
})
19 changes: 17 additions & 2 deletions opsqueue/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,25 @@ pub mod config;
/// as written in the Rust packages's `Cargo.toml`
pub const VERSION_CARGO_SEMVER: &str = env!("CARGO_PKG_VERSION");

#[allow(clippy::const_is_empty)]
/// Fixed-size buffer holding the release tag stamped into the binary during the Nix build.
///
/// Layout: `OPSQUEUE_TAG:` (13 bytes, marker) + tag value (up to 51 bytes, null-padded).
/// The Nix build patches the 51-byte tag field in the pre-compiled binary via [`stamp-tag.py`]
/// without recompiling Rust, keeping the compilation cached across tag-only changes.
///
/// [`stamp-tag.py`]: ../../stamp-tag.py
#[used]
#[unsafe(no_mangle)]
pub static OPSQUEUE_RELEASE_TAG: [u8; 64] =
*b"OPSQUEUE_TAG:dev\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";

#[must_use]
pub fn version_info() -> String {
format!("v{VERSION_CARGO_SEMVER}")
const MARKER_LEN: usize = b"OPSQUEUE_TAG:".len();
let tag = std::str::from_utf8(&OPSQUEUE_RELEASE_TAG[MARKER_LEN..])
.unwrap_or("invalid-utf8")
.trim_end_matches('\0');
format!("v{VERSION_CARGO_SEMVER} ({tag})")
}

/// Shared constant with the migrations that all the tests can reference, to avoid the generated
Expand Down
67 changes: 67 additions & 0 deletions opsqueue/stamp-tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Patch the OPSQUEUE_TAG: marker in a compiled opsqueue binary.

Usage: stamp-tag.py <binary-path> <tag>

The binary must contain the 64-byte OPSQUEUE_RELEASE_TAG symbol:
- Bytes 0..12 : b'OPSQUEUE_TAG:' (marker, unchanged)
- Bytes 13..63 : tag value, null-padded to 51 bytes (patched here)

This script is called by the Nix `withTag` build to stamp a release tag
into the pre-compiled binary without recompiling Rust.
"""

import sys

MARKER = b"OPSQUEUE_TAG:"
TOTAL_FIELD_LEN = 64
TAG_FIELD_LEN = TOTAL_FIELD_LEN - len(MARKER) # 51 bytes


def main() -> None:
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <binary-path> <tag>", file=sys.stderr)
sys.exit(1)

binary_path = sys.argv[1]
tag = sys.argv[2]

tag_bytes = tag.encode("utf-8")
if len(tag_bytes) > TAG_FIELD_LEN:
print(
f"Error: tag is {len(tag_bytes)} bytes, max is {TAG_FIELD_LEN}",
file=sys.stderr,
)
sys.exit(1)

with open(binary_path, "rb") as f:
data = f.read()

pos = data.find(MARKER)
if pos == -1:
print(f"Error: marker {MARKER!r} not found in {binary_path}", file=sys.stderr)
sys.exit(1)

second = data.find(MARKER, pos + 1)
if second != -1:
print(
f"Error: marker {MARKER!r} found more than once (at {pos} and {second})",
file=sys.stderr,
)
sys.exit(1)

padded_tag = tag_bytes.ljust(TAG_FIELD_LEN, b"\x00")
patched = data[: pos + len(MARKER)] + padded_tag + data[pos + TOTAL_FIELD_LEN :]

if len(patched) != len(data):
print("Error: patched binary has a different size", file=sys.stderr)
sys.exit(1)

with open(binary_path, "wb") as f:
f.write(patched)

print(f"Stamped tag {tag!r} into {binary_path}")


if __name__ == "__main__":
main()
Loading