Skip to content

Instantly share code, notes, and snippets.

@Malix-Labs
Last active July 23, 2026 01:56
Show Gist options
  • Select an option

  • Save Malix-Labs/4748b1b36ffa896cf0e45da84d773c38 to your computer and use it in GitHub Desktop.

Select an option

Save Malix-Labs/4748b1b36ffa896cf0e45da84d773c38 to your computer and use it in GitHub Desktop.
Nix Offline
target/
*.nar.zst

Nix Offline

nix-offline provides tooling and NixOS module configurations to enable air-gapped evaluation and building of nixpkgs and custom Nix Flakes.

Note: Commands assume Nix flakes are enabled (experimental-features = nix-command flakes).

Quick Start

Pre-cache nixpkgs source tarballs into the local /nix/store:

nix run github:Malix-Labs/nix-offline

Tip: By default, unfree packages are filtered out following standard Nixpkgs behavior. To include unfree packages, set NIXPKGS_ALLOW_UNFREE=1:

NIXPKGS_ALLOW_UNFREE=1 nix run github:Malix-Labs/nix-offline

CLI Usage

nix-offline provides a unified CLI with subcommands:

# General Syntax
nix-offline <command> [options] [arguments...]
Command Usage Description
precache nix run github:Malix-Labs/nix-offline
nix run github:Malix-Labs/nix-offline -- precache -f .
Pre-caches nixpkgs tarballs for the host system and custom Flake inputs into /nix/store.
precache --system nix run github:Malix-Labs/nix-offline -- precache -s aarch64-linux Pre-caches nixpkgs tarballs for a specified target system.
precache --all-systems nix run github:Malix-Labs/nix-offline -- precache -a -f . Pre-caches nixpkgs tarballs for all target architectures and Flake inputs.
archive export nix run github:Malix-Labs/nix-offline -- archive export -s aarch64-linux -o archive.nar.zst -f . Exports nixpkgs tarballs and custom Flake inputs to a compressed .nar.zst archive.
archive import nix run github:Malix-Labs/nix-offline -- archive import archive.nar.zst Imports a .nar.zst archive into /nix/store and pins store paths into GC roots.

Workflows

Single Machine Workflow (Online to Air-Gapped)

  1. Pre-cache system tarballs and target Flake inputs:

    nix run github:Malix-Labs/nix-offline -- precache -f .
  2. Enable nixosModules.offline in the system configuration:

    {
      inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
        nix-offline.url = "github:Malix-Labs/nix-offline";
      };
    
      outputs = { self, nixpkgs, nix-offline, ... }: {
        nixosConfigurations.my-node = nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          modules = [
            nix-offline.nixosModules.offline
            ./configuration.nix
          ];
        };
      };
    }
  3. Rebuild the system configuration:

    sudo nixos-rebuild switch --flake .#my-node

Two-Machine Workflow (Air-Gapped Node)

Machine A (Online Host)

Create and export offline archive for target host Machine B (e.g. aarch64-linux running Flake .):

nix run github:Malix-Labs/nix-offline -- archive export -s aarch64-linux -o /mnt/usb/nix-offline-archive.nar.zst -f .

Machine B (Air-Gapped Target)

  1. Import the archive directly into /nix/store using nix-offline:

    nix run github:Malix-Labs/nix-offline -- archive import /mnt/usb/nix-offline-archive.nar.zst
  2. Ensure nixosModules.offline is enabled in your system Flake to prevent substituter lookups during offline builds.

# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "clap"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "nix-offline"
version = "1.0.0"
dependencies = [
"anyhow",
"clap",
"serde",
"serde_json",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[package]
name = "nix-offline"
version = "1.0.0"
edition = "2024"
description = "Nix Offline - Tooling for air-gapped Nix evaluation, pre-caching, and archive export/import"
[[bin]]
name = "nix-offline"
path = "main.rs"
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1784288435,
"narHash": "sha256-ReRHaLgr/uVqdD8afFSn+myXIfpHeOhP0yYe0TJqAA8=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "43b3c1ab9d40fb1dbb008f451988a91e375825e9",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1784432872,
"narHash": "sha256-IX7j5TvSD7hYC9NZ8mgmq7pZ97mYyl24Qs4Tz4uyvO4=",
"rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.5591.fd1462031fde/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
"flake": false,
"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
}
{
description = "Nix Offline";
inputs = {
nixpkgs.url = "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz";
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
systems = {
url = "github:nix-systems/default";
flake = false;
};
git-hooks = {
url = "github:cachix/git-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
inputs@{
self,
flake-parts,
systems,
nixpkgs,
git-hooks,
}:
let
systems = import inputs.systems;
in
flake-parts.lib.mkFlake { inherit inputs; } {
inherit systems;
imports = [ git-hooks.flakeModule ];
flake = {
nixosModules.offline = { lib, ... }: {
nix.registry.nixpkgs.flake = nixpkgs;
nix.nixPath = [ "nixpkgs=${nixpkgs}" ];
nix.settings = {
substituters = lib.mkForce [ ];
builders-use-substitutes = false;
flake-registry = "";
};
};
nixosModules.default = self.nixosModules.offline;
};
perSystem =
{
pkgs,
system,
config,
...
}:
let
defaultBundle = "nix-offline-bundle.nar.zst";
nix-offline-pkg = pkgs.rustPlatform.buildRustPackage {
pname = "nix-offline";
version = "1.0.0";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
NIX_HOST_SYSTEM = system;
NIX_OFFLINE_NIXPKGS_PATH = "${nixpkgs}";
NIX_OFFLINE_SYSTEMS = pkgs.lib.concatStringsSep " " systems;
NIX_OFFLINE_DEFAULT_BUNDLE = defaultBundle;
nativeBuildInputs = [ pkgs.makeWrapper ];
postInstall = ''
wrapProgram $out/bin/nix-offline \
--prefix PATH : ${
pkgs.lib.makeBinPath [
pkgs.nix
pkgs.zstd
]
}
'';
meta = {
description = "Nix Offline - Tooling for air-gapped Nix evaluation, pre-caching, and archive export/import";
mainProgram = "nix-offline";
};
};
in
{
formatter = pkgs.nixfmt;
pre-commit = {
check.enable = true;
settings.hooks = {
nixfmt.enable = true;
rustfmt.enable = true;
clippy.enable = true;
};
};
packages = {
nix-offline = nix-offline-pkg;
default = nix-offline-pkg;
};
devShells.default = pkgs.mkShell {
inputsFrom = [
nix-offline-pkg
config.pre-commit.devShell
];
packages = with pkgs; [
cargo
rustc
rustfmt
clippy
rust-analyzer
];
};
};
};
}
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const DEFAULT_BUNDLE: &str = match option_env!("NIX_OFFLINE_DEFAULT_BUNDLE") {
Some(val) => val,
None => "nix-offline-bundle.nar.zst",
};
const DEFAULT_NIXPKGS_PATH: &str = match option_env!("NIX_OFFLINE_NIXPKGS_PATH") {
Some(val) => val,
None => "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz",
};
const DEFAULT_SYSTEMS_STR: Option<&'static str> = option_env!("NIX_OFFLINE_SYSTEMS");
const DEFAULT_HOST_SYSTEM: &str = match option_env!("NIX_HOST_SYSTEM") {
Some(val) => val,
None => "x86_64-linux",
};
#[derive(Parser, Debug)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Pre-cache nixpkgs tarballs and/or Flake inputs into /nix/store
Precache(PrecacheArgs),
/// Export or import compressed .nar.zst archives
Archive {
#[command(subcommand)]
action: ArchiveAction,
},
}
#[derive(Parser, Debug)]
struct PrecacheArgs {
/// Pre-cache nixpkgs tarballs for specific system architecture
#[arg(short, long)]
system: Option<String>,
/// Pre-cache nixpkgs tarballs for all supported system architectures
#[arg(short = 'a', long)]
all_systems: bool,
/// Flake path(s) to archive and GC-pin
#[arg(short, long)]
flake: Vec<String>,
/// Skip Flake input pre-caching (only pre-cache nixpkgs tarballs)
#[arg(long)]
no_flake: bool,
/// Skip nixpkgs tarball pre-caching (only pre-cache Flake inputs)
#[arg(long)]
no_nixpkgs: bool,
}
impl PrecacheArgs {
fn flake_paths(&self) -> Vec<String> {
if self.flake.is_empty() {
vec![".".to_string()]
} else {
self.flake.clone()
}
}
}
#[derive(Subcommand, Debug)]
enum ArchiveAction {
/// Export nixpkgs tarballs and/or Flake inputs to a compressed .nar.zst archive
Export(ExportArgs),
/// Import a .nar.zst archive into /nix/store and pin GC roots
Import(ImportArgs),
}
#[derive(Parser, Debug)]
struct ExportArgs {
/// Include nixpkgs tarballs for specific target system architecture
#[arg(short, long)]
system: Option<String>,
/// Include nixpkgs tarballs for all supported system architectures
#[arg(short = 'a', long)]
all_systems: bool,
/// Flake path(s) to include
#[arg(short, long)]
flake: Vec<String>,
/// Output .nar.zst bundle path
#[arg(short, long, default_value = DEFAULT_BUNDLE)]
output: String,
/// Do not include Flake inputs in exported archive
#[arg(long)]
no_flake: bool,
/// Do not include nixpkgs tarballs in exported archive
#[arg(long)]
no_nixpkgs: bool,
}
impl ExportArgs {
fn flake_paths(&self) -> Vec<String> {
if self.flake.is_empty() {
vec![".".to_string()]
} else {
self.flake.clone()
}
}
}
#[derive(Parser, Debug)]
struct ImportArgs {
/// Compressed .nar.zst archive file
#[arg(default_value = DEFAULT_BUNDLE)]
file: String,
}
#[derive(Deserialize)]
struct FlakeArchiveOutput {
path: Option<String>,
inputs: Option<HashMap<String, FlakeInput>>,
}
#[derive(Deserialize)]
struct FlakeInput {
path: Option<String>,
}
fn get_nix_flags() -> Vec<String> {
let mut flags = vec![
"--option".to_string(),
"hashed-mirrors".to_string(),
"https://tarballs.nixos.org".to_string(),
"--option".to_string(),
"http-connections".to_string(),
env::var("NIX_HTTP_CONNECTIONS").unwrap_or_else(|_| "16".to_string()),
];
let token = env::var("GITHUB_TOKEN")
.or_else(|_| env::var("GH_TOKEN"))
.unwrap_or_default();
if !token.is_empty() {
flags.push("--option".to_string());
flags.push("access-tokens".to_string());
flags.push(format!("github.com={}", token));
}
flags
}
fn get_gc_dir() -> Result<PathBuf> {
let state_home = env::var("XDG_STATE_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
format!("{}/.local/state", home)
});
let gc_dir = PathBuf::from(state_home).join("nix").join("gcroots");
fs::create_dir_all(&gc_dir).context("Failed to create GC roots directory")?;
Ok(gc_dir)
}
fn get_host_system() -> String {
env::var("NIX_HOST_SYSTEM").unwrap_or_else(|_| DEFAULT_HOST_SYSTEM.to_string())
}
fn eval_expr(sys: &str, nixpkgs_path: &str) -> String {
format!(
"let pkgs = import {} {{ system = \"{}\"; }}; in import {}/maintainers/scripts/all-tarballs.nix {{ inherit pkgs; }}",
nixpkgs_path, sys, nixpkgs_path
)
}
fn build_system_tarballs(systems: &[String], nixpkgs_path: &str) -> Result<Vec<String>> {
let mut tarballs = Vec::new();
let flags = get_nix_flags();
for sys in systems {
eprintln!("==> Building nixpkgs tarballs for {}...", sys);
let expr = eval_expr(sys, nixpkgs_path);
let mut cmd = Command::new("nix");
cmd.arg("build")
.arg("--impure")
.args(&flags)
.arg("--print-out-paths")
.arg("--expr")
.arg(&expr);
let output = cmd
.output()
.context("Failed to run nix build for tarballs")?;
if !output.status.success() {
bail!(
"nix build failed for system {}: {}",
sys,
String::from_utf8_lossy(&output.stderr)
);
}
let paths_str = String::from_utf8_lossy(&output.stdout);
for line in paths_str.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
tarballs.push(trimmed.to_string());
}
}
}
Ok(tarballs)
}
fn collect_flake_paths(flakes: &[String]) -> Result<Vec<String>> {
let mut store_paths = Vec::new();
let flags = get_nix_flags();
let target_flakes = if flakes.is_empty() {
vec![".".to_string()]
} else {
flakes.to_vec()
};
for f in &target_flakes {
if f.is_empty() {
continue;
}
let mut cmd = Command::new("nix");
cmd.arg("flake")
.arg("archive")
.args(&flags)
.arg("--json")
.arg(f);
if let Ok(out) = cmd.output() {
if !out.status.success() {
continue;
}
if let Ok(parsed) = serde_json::from_slice::<FlakeArchiveOutput>(&out.stdout) {
if let Some(p) = &parsed.path {
store_paths.push(p.clone());
}
if let Some(inputs) = &parsed.inputs {
for input in inputs.values() {
if let Some(ip) = &input.path {
store_paths.push(ip.clone());
}
}
}
}
}
}
Ok(store_paths)
}
fn run_precache(
args: PrecacheArgs,
nixpkgs_path: &str,
supported_systems: &[String],
) -> Result<()> {
let gc_dir = get_gc_dir()?;
let flags = get_nix_flags();
if !args.no_nixpkgs {
let systems_to_cache = if args.all_systems {
supported_systems.to_vec()
} else {
vec![args.system.clone().unwrap_or_else(get_host_system)]
};
for sys in &systems_to_cache {
eprintln!("==> Pre-caching nixpkgs tarballs for {}...", sys);
let expr = eval_expr(sys, nixpkgs_path);
let out_link = gc_dir.join(format!("nix-offline-{}", sys));
let mut cmd = Command::new("nix");
cmd.arg("build")
.arg("--impure")
.args(&flags)
.arg("--out-link")
.arg(&out_link)
.arg("--expr")
.arg(&expr);
let status = cmd.status().context("Failed to run nix build")?;
if !status.success() {
bail!("nix build failed for system {}", sys);
}
}
}
if !args.no_flake {
let store_paths = collect_flake_paths(&args.flake_paths())?;
if !store_paths.is_empty() {
eprintln!("==> GC-pinning Flake inputs into {}...", gc_dir.display());
let target_pin_dir = gc_dir.join("nix-offline-flake");
let mut cmd = Command::new("nix");
cmd.arg("store")
.arg("pin")
.arg("--into")
.arg(&target_pin_dir)
.args(&store_paths);
let _ = cmd.status();
}
}
Ok(())
}
fn run_export(args: ExportArgs, nixpkgs_path: &str, supported_systems: &[String]) -> Result<()> {
let target_sys = args.system.clone().unwrap_or_else(get_host_system);
let output_file = args.output.clone();
let mut paths_to_export = Vec::new();
if !args.no_nixpkgs {
let systems_to_export = if args.all_systems {
supported_systems.to_vec()
} else {
vec![target_sys]
};
let tarball_paths = build_system_tarballs(&systems_to_export, nixpkgs_path)?;
paths_to_export.extend(tarball_paths);
paths_to_export.push(nixpkgs_path.to_string());
}
if !args.no_flake {
let flake_paths = collect_flake_paths(&args.flake_paths())?;
paths_to_export.extend(flake_paths);
}
eprintln!(
"==> Exporting {} store paths to {} using zstd...",
paths_to_export.len(),
output_file
);
let mut nix_export = Command::new("nix")
.arg("store")
.arg("export")
.args(&paths_to_export)
.stdout(Stdio::piped())
.spawn()
.context("Failed to execute nix store export")?;
let export_stdout = nix_export
.stdout
.take()
.context("Failed to capture nix store export stdout")?;
let zstd_status = Command::new("zstd")
.arg("-T0")
.arg("-o")
.arg(&output_file)
.stdin(export_stdout)
.status()
.context("Failed to execute zstd compression")?;
let nix_status = nix_export.wait().context("nix store export failed")?;
if !nix_status.success() || !zstd_status.success() {
bail!("Archive export failed!");
}
eprintln!("==> Export completed successfully: {}", output_file);
Ok(())
}
fn run_import(args: ImportArgs) -> Result<()> {
let gc_dir = get_gc_dir()?;
let path = Path::new(&args.file);
if !path.exists() {
bail!("Archive file '{}' not found.", args.file);
}
eprintln!("==> Importing {} into /nix/store...", args.file);
let mut zstd_cmd = Command::new("zstd")
.arg("-dc")
.arg(&args.file)
.stdout(Stdio::piped())
.spawn()
.context("Failed to execute zstd decompression")?;
let zstd_stdout = zstd_cmd
.stdout
.take()
.context("Failed to capture zstd stdout")?;
let nix_import_output = Command::new("nix")
.arg("store")
.arg("import")
.stdin(zstd_stdout)
.output()
.context("Failed to execute nix store import")?;
let zstd_status = zstd_cmd.wait().context("zstd decompression failed")?;
if !zstd_status.success() || !nix_import_output.status.success() {
bail!(
"Archive import failed: {}",
String::from_utf8_lossy(&nix_import_output.stderr)
);
}
let imported_paths_str = String::from_utf8_lossy(&nix_import_output.stdout);
let imported_paths: Vec<&str> = imported_paths_str
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect();
if !imported_paths.is_empty() {
let target_pin_dir = gc_dir.join("nix-offline-imported");
let mut cmd = Command::new("nix");
cmd.arg("store")
.arg("pin")
.arg("--into")
.arg(&target_pin_dir)
.args(&imported_paths);
let _ = cmd.status();
}
eprintln!(
"==> Done. Store paths imported and GC-pinned into {}",
gc_dir.display()
);
Ok(())
}
fn main() -> Result<()> {
let nixpkgs_path =
env::var("NIX_OFFLINE_NIXPKGS_PATH").unwrap_or_else(|_| DEFAULT_NIXPKGS_PATH.to_string());
let supported_systems: Vec<String> = env::var("NIX_OFFLINE_SYSTEMS")
.map(|s| s.split_whitespace().map(|x| x.to_string()).collect())
.unwrap_or_else(|_| {
DEFAULT_SYSTEMS_STR
.map(|s| s.split_whitespace().map(|x| x.to_string()).collect())
.unwrap_or_else(|| {
vec![
"aarch64-darwin".to_string(),
"aarch64-linux".to_string(),
"x86_64-darwin".to_string(),
"x86_64-linux".to_string(),
]
})
});
let cli = Cli::parse();
match cli.command {
Some(Commands::Precache(args)) => run_precache(args, &nixpkgs_path, &supported_systems),
Some(Commands::Archive { action }) => match action {
ArchiveAction::Export(args) => run_export(args, &nixpkgs_path, &supported_systems),
ArchiveAction::Import(args) => run_import(args),
},
None => run_precache(
PrecacheArgs {
system: None,
all_systems: false,
flake: vec![".".to_string()],
no_flake: false,
no_nixpkgs: false,
},
&nixpkgs_path,
&supported_systems,
),
}
}
@Malix-Labs

Copy link
Copy Markdown
Author

Issue: make host follows the nixpkgs input

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment