Skip to main content
Glama
nix_binary_build.py8.35 kB
#!/usr/bin/env python3 """ Builds a binary built with Nix but not dependent on Nix packages. """ import argparse import os from pathlib import Path import shutil import subprocess import json import sys from enum import Enum, EnumMeta import tempfile from typing import Any, Dict, List, Union # A slightly more Rust-y feeling enum # Thanks to: https://stackoverflow.com/a/65225753 class MetaEnum(EnumMeta): def __contains__(self: type[Any], member: object) -> bool: try: self(member) except ValueError: return False return True class BaseEnum(Enum, metaclass=MetaEnum): pass class PlatformArchitecture(BaseEnum): Aarch64 = "aarch64" X86_64 = "x86_64" class PlatformOS(BaseEnum): Darwin = "darwin" Linux = "linux" Windows = "windows" VARIANT = "binary" NIX_PKG_NAME_SUFFIX = "-standalone" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--git-info-json", required=True, help="Path to the Git metadata JSON file", ) parser.add_argument( "--artifact-out-file", required=True, help="Path to write the binary artifact file", ) parser.add_argument( "--build-metadata-out-file", required=True, help="Path to write the build metadata JSON file", ) parser.add_argument( "--binary-metadata-out-file", required=True, help="Path to write the binary metadata JSON file", ) parser.add_argument( "--build-context-dir", required=True, help="Path to build context directory", ) parser.add_argument( "--name", required=True, help="Name of binary to build", ) parser.add_argument( "--author", required=True, help="Author to be used in binary metadata", ) parser.add_argument( "--source-url", required=True, help="Source code URL to be used in binary metadata", ) parser.add_argument( "--license", required=True, help="Image license to be used in binary metadata", ) return parser.parse_args() def main() -> int: args = parse_args() git_info = load_git_info(args.git_info_json) architecture = detect_architecture() os = detect_os() nix_store_pkg_path = build_nix_binary(args.name, args.build_context_dir) binary_metadata = compute_binary_metadata( git_info, args.name, args.author, args.source_url, args.license, architecture, os, ) write_binary( args.name, Path(args.artifact_out_file), Path(nix_store_pkg_path), ) write_json(args.binary_metadata_out_file, binary_metadata) b3sum = compute_b3sum(args.artifact_out_file) build_metadata = compute_build_metadata( git_info, args.name, architecture, os, b3sum, ) write_json(args.build_metadata_out_file, build_metadata) return 0 def build_nix_binary(name: str, build_context_dir: str) -> str: cmd = [ "nix", "build", "--extra-experimental-features", "nix-command flakes impure-derivations ca-derivations", "--option", "filter-syscalls", "false", "--print-build-logs", f".#{name}{NIX_PKG_NAME_SUFFIX}", ] # We are purposefully escaping the default `$TMPDIR` location as Buck2 sets # `$TMPDIR` to be under the `buck-out/` directory. Unfortunately (for us in # this context), `nix build` will recursely look up the directory tree # searching for a `.git/` directory so we avoid this by running our build # under the system's `/tmp/` directory. Sorry folks! with tempfile.TemporaryDirectory( prefix="/tmp/nix_binary_build-") as tempdir: root_dir = os.path.join(tempdir, "root") # Copy build context into tempdir shutil.copytree( build_context_dir, root_dir, symlinks=True, ) # If we find a workspace dir, let's get the contents into the root workspace = os.path.join(root_dir, "workspace") if os.path.isdir(workspace): print("--- Found a workspace dir, moving contents into root") move_contents_up(workspace) print("--- Build nix package with: '{}'".format(" ".join(cmd))) # Create parent directories subprocess.run(cmd, cwd=root_dir).check_returncode() nix_store_pkg_path = os.readlink(os.path.join(root_dir, "result")) return nix_store_pkg_path def write_binary(name: str, out_file: Path, nix_store_pkg_path: Path): src = nix_store_pkg_path.joinpath("bin", name) shutil.copyfile(src, out_file) shutil.copymode(src, out_file) def write_json(output: str, metadata: Union[Dict[str, str], List[str]]): with open(output, "w") as file: json.dump(metadata, file, sort_keys=True) # Possible machine architecture detection comes from reading the Rustup shell # script installer--thank you for your service! # See: https://github.com/rust-lang/rustup/blob/master/rustup-init.sh def detect_architecture() -> PlatformArchitecture: machine = os.uname().machine if (machine == "amd64" or machine == "x86_64" or machine == "x86-64" or machine == "x64"): return PlatformArchitecture.X86_64 elif (machine == "arm64" or machine == "aarch64" or machine == "arm64v8"): return PlatformArchitecture.Aarch64 else: print( f"xxx Failed to determine architecure or unsupported: {machine}", file=sys.stderr, ) sys.exit(1) # Possible machine operating system detection comes from reading the Rustup shell # script installer--thank you for your service! # See: https://github.com/rust-lang/rustup/blob/master/rustup-init.sh def detect_os() -> PlatformOS: platform_os = os.uname().sysname match platform_os: case "Darwin": return PlatformOS.Darwin case "Linux": return PlatformOS.Linux case "Windows": return PlatformOS.Windows case _: print( f"xxx Failed to determine operating system or unsupported: {platform_os}", file=sys.stderr, ) sys.exit(1) def load_git_info(git_info_file: str) -> Dict[str, str | int | bool]: with open(git_info_file) as file: return json.load(file) def compute_binary_metadata( git_info: Dict[str, str | int | bool], name: str, author: str, source_url: str, license: str, architecture: PlatformArchitecture, platform_os: PlatformOS, ) -> Dict[str, str]: metadata = { "name": name, "version": git_info.get("canonical_version"), "author": author, "source_url": source_url, "license": license, "architecture": architecture.value, "os": platform_os.value, "commit": git_info.get("commit_hash"), "branch": git_info.get("branch"), } return metadata def compute_b3sum(artifact_file: str) -> str: cmd = [ "b3sum", "--no-names", artifact_file, ] result = subprocess.run(cmd, capture_output=True) # Print out stderr from process if it failed if result.returncode != 0: sys.stderr.write(result.stderr.decode("ascii")) result.check_returncode() b3sum = result.stdout.decode("ascii").rstrip() return b3sum def compute_build_metadata( git_info: Dict[str, str | int | bool], family: str, platform_arch: PlatformArchitecture, platform_os: PlatformOS, b3sum: str, ) -> Dict[str, str]: metadata = { "family": family, "variant": VARIANT, "version": git_info.get("canonical_version"), "arch": platform_arch.value, "os": platform_os.value, "commit": git_info.get("commit_hash"), "branch": git_info.get("branch"), "b3sum": b3sum, } return metadata def move_contents_up(directory): parent_dir = os.path.dirname(directory) shutil.copytree(directory, parent_dir, dirs_exist_ok=True) shutil.rmtree(directory) if __name__ == "__main__": sys.exit(main())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server