diff --git a/README.md b/README.md index b0aab749..0e88c5bf 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,9 @@ System dependencies ``` # NixOS nix-shell --run fish -p cargo systemd udev hidapi pkg-config -direnv shell +# NixOS using flakes +nix develop +# Or build fully using flake, see section below # Fedora sudo dnf install systemd-devel hidapi-devel @@ -179,6 +181,40 @@ sudo pkg install rust hidapi pkgconf sudo apt install rustup build-essential libhidapi-dev libsystemd-dev libudev-dev libusb-1.0-0-dev pkg-config ``` +### Nix Flake + +If you have Nix with flakes enabled, you can build and develop without manually installing dependencies. + +Run directly from GitHub +```sh +# Run directly from GitHub +nix run github.com:FrameworkComputer/framework-system -- --versions + + +Build and run locally after cloning: + +``` +# Build the CLI tool (release) +nix build .#tool + +# Build the CLI tool (debug) +nix build .#tool-debug + +# Build the UEFI application (release) +nix build .#uefi + +# Run the CLI tool directly +nix run .#tool -- --help + +# Run the UEFI app in QEMU +nix run .#qemu + +# Enter a development shell with all dependencies +nix develop +``` + +### Building with Cargo + ```sh # Running linter cargo clippy diff --git a/devenv.lock b/devenv.lock deleted file mode 100644 index 4f9c5ab8..00000000 --- a/devenv.lock +++ /dev/null @@ -1,160 +0,0 @@ -{ - "nodes": { - "devenv": { - "locked": { - "dir": "src/modules", - "lastModified": 1769263214, - "owner": "cachix", - "repo": "devenv", - "rev": "328752c05f1745c7f613689a9df0bd1ffc8f3922", - "type": "github" - }, - "original": { - "dir": "src/modules", - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "fenix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1769323446, - "owner": "nix-community", - "repo": "fenix", - "rev": "f1cf6dcfc51a08e154ec753f11146dc1d95be1e5", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1767039857, - "owner": "NixOS", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1769069492, - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762808025, - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1767052823, - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "538a5124359f0b3d466e1160378c87887e3b51a4", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "devenv": "devenv", - "fenix": "fenix", - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": [ - "git-hooks" - ], - "rust-overlay": "rust-overlay" - } - }, - "rust-analyzer-src": { - "flake": false, - "locked": { - "lastModified": 1769335258, - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "2532c48f1ed25de1b90d0287c364ee4f306bec0e", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1769396217, - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "e9bcd12156a577ac4e47d131c14dc0293cc9c8c2", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/devenv.nix b/devenv.nix deleted file mode 100644 index e84d1529..00000000 --- a/devenv.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ pkgs, lib, config, inputs, ... }: - -{ - packages = with pkgs; [ - systemd # libudev - # For UEFI building and testing - parted - gnumake - qemu - ]; - - languages.rust = { - enable = true; - targets = [ "x86_64-unknown-uefi" ]; - # https://devenv.sh/reference/options/#languagesrustchannel - channel = "stable"; - }; -} diff --git a/devenv.yaml b/devenv.yaml deleted file mode 100644 index 332d3264..00000000 --- a/devenv.yaml +++ /dev/null @@ -1,13 +0,0 @@ -inputs: - fenix: - url: github:nix-community/fenix - inputs: - nixpkgs: - follows: nixpkgs - nixpkgs: - url: github:cachix/devenv-nixpkgs/rolling - rust-overlay: - url: github:oxalica/rust-overlay - inputs: - nixpkgs: - follows: nixpkgs diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..87ca7ac6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,82 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1770689213, + "narHash": "sha256-N6JiSpfi0s8NjUTnjwo3c+YAmvYhCDzjCKCrTUC97xM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "49d75834011c94a120a9cb874ac1c4d8b7bfc767", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770779462, + "narHash": "sha256-ykcXTKtV+dOaKlOidAj6dpewBHjni9/oy/6VKcqfzfY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8a53b3ade61914cdb10387db991b90a3a6f3c441", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..0aa9abf6 --- /dev/null +++ b/flake.nix @@ -0,0 +1,312 @@ +{ + description = "Framework System library and CLI tool for Framework Computer hardware"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + + # Read toolchain from rust-toolchain.toml + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + + # Create a custom rustPlatform with our toolchain + rustPlatform = pkgs.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }; + + # Common build inputs for OS builds + commonBuildInputs = with pkgs; [ + openssl + libgit2 + zlib + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + systemdLibs # libudev + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + pkgs.libiconv + pkgs.darwin.apple_sdk.frameworks.Security + pkgs.darwin.apple_sdk.frameworks.SystemConfiguration + ]; + + commonNativeBuildInputs = with pkgs; [ + pkg-config + zlib # Required by framework_lib build script at runtime + ]; + + # Filter source to only include files needed for the build + buildSrc = pkgs.lib.cleanSourceWith { + src = ./.; + filter = path: type: + let + baseName = baseNameOf path; + relativePath = pkgs.lib.removePrefix (toString ./. + "/") path; + # Only include files/folders needed for the Rust build + includedRoots = [ + "framework_lib" + "framework_tool" + "framework_uefi" + ".cargo" + ]; + includedFiles = [ + "Cargo.toml" + "Cargo.lock" + "rust-toolchain.toml" + ]; + isIncludedRoot = builtins.any (root: + relativePath == root || pkgs.lib.hasPrefix (root + "/") relativePath + ) includedRoots; + in + isIncludedRoot || builtins.elem baseName includedFiles; + }; + + # Git dependency output hashes + gitDependencyHashes = { + "redox_hwio-0.1.6" = "sha256-knLIZ7yp42SQYk32NGq3SUGvJFVumFhD64Njr5TRdFs="; + "smbios-lib-0.9.1" = "sha256-3L8JaA75j9Aaqg1z9lVs61m6CvXDeQprEFRq+UDCHQo="; + "uefi-0.20.0" = "sha256-2lUd2a+7NvS94LyAHE2BwGV4j6607mbPXE5htrwdz04="; + }; + + # Build function for the CLI tool (Linux/macOS) + buildFrameworkTool = { release ? false, features ? [] }: + let + profile = if release then "release" else "debug"; + featuresStr = if features == [] then "" else "--features ${builtins.concatStringsSep "," features}"; + in + rustPlatform.buildRustPackage { + pname = "framework_tool"; + version = "0.5.0"; + + src = buildSrc; + + cargoLock = { + lockFile = ./Cargo.lock; + outputHashes = gitDependencyHashes; + }; + + buildType = profile; + + # Build only the tool, not the UEFI package + buildPhase = '' + runHook preBuild + cargo build \ + ${if release then "--release" else ""} \ + -p framework_tool \ + ${featuresStr} + runHook postBuild + ''; + + # Run tests for framework_lib + checkPhase = '' + runHook preCheck + cargo test -p framework_lib + runHook postCheck + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out/bin + cp target/${profile}/framework_tool $out/bin/ + runHook postInstall + ''; + + nativeBuildInputs = commonNativeBuildInputs; + buildInputs = commonBuildInputs; + + # Environment variables for C library bindings + OPENSSL_NO_VENDOR = "1"; + LIBGIT2_NO_VENDOR = "1"; + }; + + # Build function for UEFI application + buildFrameworkUefi = { release ? false, features ? [] }: + let + profile = if release then "release" else "debug"; + featuresStr = if features == [] then "" else "--features ${builtins.concatStringsSep "," features}"; + in + rustPlatform.buildRustPackage { + pname = "framework_uefi"; + version = "0.5.0"; + + src = buildSrc; + + cargoLock = { + lockFile = ./Cargo.lock; + outputHashes = gitDependencyHashes; + }; + + buildType = profile; + buildNoDefaultFeatures = true; + + # Disable cargo-auditable as it's incompatible with UEFI linker + auditable = false; + + # Target for UEFI - passed via args to avoid affecting build scripts + buildPhase = '' + runHook preBuild + cargo build \ + ${if release then "--release" else ""} \ + --target x86_64-unknown-uefi \ + -p framework_uefi \ + ${featuresStr} + runHook postBuild + ''; + + # Skip check phase - UEFI binaries can't be tested on host + doCheck = false; + + installPhase = '' + runHook preInstall + mkdir -p $out/bin + cp target/x86_64-unknown-uefi/${profile}/uefitool.efi $out/bin/ + runHook postInstall + ''; + + nativeBuildInputs = commonNativeBuildInputs; + buildInputs = commonBuildInputs; + + # Environment variables for C library bindings + OPENSSL_NO_VENDOR = "1"; + LIBGIT2_NO_VENDOR = "1"; + }; + + # Package definitions + framework-tool-debug = buildFrameworkTool { release = false; }; + framework-tool-release = buildFrameworkTool { release = true; }; + framework-uefi-debug = buildFrameworkUefi { release = false; }; + framework-uefi-release = buildFrameworkUefi { release = true; }; + + # Wrapper script to run the UEFI build in an emulator + run-qemu = pkgs.writeShellScriptBin "run-framework-uefi-qemu" '' + set -e + + # Create temporary directory for ESP and OVMF vars + TMPDIR=$(mktemp -d) + trap "rm -rf $TMPDIR" EXIT + + # Set up ESP filesystem structure + mkdir -p "$TMPDIR/esp/efi/boot" + cp ${framework-uefi-debug}/bin/uefitool.efi "$TMPDIR/esp/efi/boot/bootx64.efi" + + # Copy OVMF_VARS.fd to temp (it needs to be writable) + cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd "$TMPDIR/OVMF_VARS.fd" + chmod 644 "$TMPDIR/OVMF_VARS.fd" + + echo "Starting QEMU with Framework UEFI tool..." + echo "Debug output will be written to: $TMPDIR/debug.log" + + ${pkgs.qemu}/bin/qemu-system-x86_64 \ + ''${QEMU_KVM:+-enable-kvm} \ + -M q35 \ + -m 1024 \ + -net none \ + -serial stdio \ + -debugcon "file:$TMPDIR/debug.log" -global isa-debugcon.iobase=0x402 \ + -usb \ + -drive if=pflash,format=raw,readonly=on,file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd \ + -drive if=pflash,format=raw,readonly=off,file="$TMPDIR/OVMF_VARS.fd" \ + -drive format=raw,file=fat:rw:"$TMPDIR/esp" \ + "$@" + ''; + + # Wrapper for release QEMU build + run-qemu-release = pkgs.writeShellScriptBin "run-framework-uefi-qemu" '' + set -e + + TMPDIR=$(mktemp -d) + trap "rm -rf $TMPDIR" EXIT + + mkdir -p "$TMPDIR/esp/efi/boot" + cp ${framework-uefi-release}/bin/uefitool.efi "$TMPDIR/esp/efi/boot/bootx64.efi" + + cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd "$TMPDIR/OVMF_VARS.fd" + chmod 644 "$TMPDIR/OVMF_VARS.fd" + + echo "Starting QEMU with Framework UEFI tool (release)..." + + ${pkgs.qemu}/bin/qemu-system-x86_64 \ + ''${QEMU_KVM:+-enable-kvm} \ + -M q35 \ + -m 1024 \ + -net none \ + -serial stdio \ + -debugcon "file:$TMPDIR/debug.log" -global isa-debugcon.iobase=0x402 \ + -usb \ + -drive if=pflash,format=raw,readonly=on,file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd \ + -drive if=pflash,format=raw,readonly=off,file="$TMPDIR/OVMF_VARS.fd" \ + -drive format=raw,file=fat:rw:"$TMPDIR/esp" \ + "$@" + ''; + + in + { + checks = { + inherit framework-tool-release framework-uefi-release; + }; + + packages = { + default = framework-tool-release; + tool = framework-tool-release; + tool-debug = framework-tool-debug; + uefi = framework-uefi-release; + uefi-debug = framework-uefi-debug; + run-qemu = run-qemu; + run-qemu-release = run-qemu-release; + }; + + # Convenience apps for `nix run` + apps = { + default = flake-utils.lib.mkApp { drv = framework-tool-release; exePath = "/bin/framework_tool"; }; + tool = flake-utils.lib.mkApp { drv = framework-tool-release; exePath = "/bin/framework_tool"; }; + qemu = flake-utils.lib.mkApp { drv = run-qemu; }; + qemu-release = flake-utils.lib.mkApp { drv = run-qemu-release; }; + }; + + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + rustToolchain + + # Development tools + gnumake + qemu + parted + OVMF + + # Build dependencies + pkg-config + openssl + libgit2 + zlib + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + systemdLibs # libudev + ]; + + OPENSSL_NO_VENDOR = "1"; + LIBGIT2_NO_VENDOR = "1"; + + # Set up OVMF symlinks for `make run` compatibility + shellHook = '' + if [ ! -d ovmf ] || [ ! -e ovmf/OVMF_CODE.fd ] || [ ! -e ovmf/OVMF_VARS.fd ]; then + mkdir -p ovmf + ln -sf ${pkgs.OVMF.fd}/FV/OVMF_CODE.fd ovmf/OVMF_CODE.fd + # OVMF_VARS needs to be writable, so copy it instead of symlinking + cp -f ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd ovmf/OVMF_VARS.fd + chmod 644 ovmf/OVMF_VARS.fd + echo "OVMF files set up in ovmf/" + fi + ''; + }; + } + ); +}