fix(nix): enable sandboxed builds with bun2nix (#199)

* fix(nix): enable sandboxed builds with bun2nix

The Nix package was broken on Linux because `bun install` requires
network access, which is blocked by Nix sandboxing (enabled by default
on Linux).

This switches to bun2nix for dependency management:
- Add bun2nix flake input to pre-fetch all npm dependencies
- Generate bun.nix lockfile for reproducible dependency resolution
- Copy bun cache to writable location during build to avoid EACCES
  errors from bunx writing to the read-only Nix store
- Add nanoid as an explicit dependency (was imported directly but only
  available as a transitive dep, which breaks with isolated linker)
- Update CI workflow to perform a full sandboxed build
- Add bun2nix to devShell for easy lockfile regeneration

Closes #197

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(nix): create writable workdir for database access

The app uses process.cwd()/data for the database path, but when running
from the Nix store the cwd is read-only. Create a writable working
directory with symlinks to app files and a real data directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ARUNAVO RAY
2026-02-27 12:43:37 +05:30
committed by GitHub
parent d0efa200d9
commit 5aa0f3260d
6 changed files with 3969 additions and 90 deletions

View File

@@ -33,13 +33,5 @@ jobs:
- name: Show flake info - name: Show flake info
run: nix flake show run: nix flake show
- name: Evaluate package - name: Build package
run: | run: nix build --print-build-logs
# Evaluate the derivation without building (validates the Nix expression)
nix eval .#packages.$(nix eval --impure --expr 'builtins.currentSystem').default.name
echo "Flake evaluation successful"
# Note: Full build requires network access for bun install.
# Nix sandboxed builds block network access.
# To build locally: nix build --option sandbox false
# Or use: nix develop && bun install && bun run build

133
bun.lock
View File

@@ -5,74 +5,75 @@
"": { "": {
"name": "gitea-mirror", "name": "gitea-mirror",
"dependencies": { "dependencies": {
"@astrojs/check": "latest", "@astrojs/check": "^0.9.6",
"@astrojs/mdx": "latest", "@astrojs/mdx": "4.3.13",
"@astrojs/node": "latest", "@astrojs/node": "9.5.4",
"@astrojs/react": "latest", "@astrojs/react": "^4.4.2",
"@better-auth/sso": "latest", "@better-auth/sso": "1.4.19",
"@octokit/plugin-throttling": "latest", "@octokit/plugin-throttling": "^11.0.3",
"@octokit/rest": "latest", "@octokit/rest": "^22.0.1",
"@radix-ui/react-accordion": "latest", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "latest", "@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "latest", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "latest", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "latest", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "latest", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "latest", "@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "latest", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "latest", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "latest", "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "latest", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "latest", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "latest", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "latest", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "latest", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "latest", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "latest", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "latest", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "latest", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-virtual": "latest", "@tanstack/react-virtual": "^3.13.19",
"@types/canvas-confetti": "latest", "@types/canvas-confetti": "^1.9.0",
"@types/react": "latest", "@types/react": "^19.2.14",
"@types/react-dom": "latest", "@types/react-dom": "^19.2.3",
"astro": "latest", "astro": "^5.17.3",
"bcryptjs": "latest", "bcryptjs": "^3.0.3",
"better-auth": "latest", "better-auth": "1.4.19",
"buffer": "latest", "buffer": "^6.0.3",
"canvas-confetti": "latest", "canvas-confetti": "^1.9.4",
"class-variance-authority": "latest", "class-variance-authority": "^0.7.1",
"clsx": "latest", "clsx": "^2.1.1",
"cmdk": "latest", "cmdk": "^1.1.1",
"dotenv": "latest", "dotenv": "^17.3.1",
"drizzle-orm": "latest", "drizzle-orm": "^0.45.1",
"fuse.js": "latest", "fuse.js": "^7.1.0",
"jsonwebtoken": "latest", "jsonwebtoken": "^9.0.3",
"lucide-react": "latest", "lucide-react": "^0.575.0",
"next-themes": "latest", "nanoid": "^3.3.11",
"react": "latest", "next-themes": "^0.4.6",
"react-dom": "latest", "react": "^19.2.4",
"react-icons": "latest", "react-dom": "^19.2.4",
"sonner": "latest", "react-icons": "^5.5.0",
"tailwind-merge": "latest", "sonner": "^2.0.7",
"tailwindcss": "latest", "tailwind-merge": "^3.5.0",
"tw-animate-css": "latest", "tailwindcss": "^4.2.1",
"typescript": "latest", "tw-animate-css": "^1.4.0",
"uuid": "latest", "typescript": "^5.9.3",
"vaul": "latest", "uuid": "^13.0.0",
"zod": "latest", "vaul": "^1.1.2",
"zod": "^4.3.6",
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "latest", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "latest", "@testing-library/react": "^16.3.2",
"@types/bcryptjs": "latest", "@types/bcryptjs": "^3.0.0",
"@types/bun": "latest", "@types/bun": "^1.3.9",
"@types/jsonwebtoken": "latest", "@types/jsonwebtoken": "^9.0.10",
"@types/uuid": "latest", "@types/uuid": "^11.0.0",
"@vitejs/plugin-react": "latest", "@vitejs/plugin-react": "^5.1.4",
"drizzle-kit": "latest", "drizzle-kit": "^0.31.9",
"jsdom": "latest", "jsdom": "^28.1.0",
"tsx": "latest", "tsx": "^4.21.0",
"vitest": "latest", "vitest": "^4.0.18",
}, },
}, },
}, },

3724
bun.nix Normal file

File diff suppressed because it is too large Load Diff

111
flake.lock generated
View File

@@ -1,8 +1,50 @@
{ {
"nodes": { "nodes": {
"bun2nix": {
"inputs": {
"flake-parts": "flake-parts",
"import-tree": "import-tree",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1770895533,
"narHash": "sha256-v3QaK9ugy9bN9RXDnjw0i2OifKmz2NnKM82agtqm/UY=",
"owner": "nix-community",
"repo": "bun2nix",
"rev": "c843f477b15f51151f8c6bcc886954699440a6e1",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "bun2nix",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1769996383,
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems_2"
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1731533236,
@@ -18,6 +60,21 @@
"type": "github" "type": "github"
} }
}, },
"import-tree": {
"locked": {
"lastModified": 1763762820,
"narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=",
"owner": "vic",
"repo": "import-tree",
"rev": "3c23749d8013ec6daa1d7255057590e9ca726646",
"type": "github"
},
"original": {
"owner": "vic",
"repo": "import-tree",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1761672384, "lastModified": 1761672384,
@@ -34,8 +91,24 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"lastModified": 1769909678,
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "72716169fe93074c333e8d0173151350670b824c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"bun2nix": "bun2nix",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
@@ -54,6 +127,42 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"bun2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1770228511,
"narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@@ -1,18 +1,32 @@
{ {
description = "Gitea Mirror - Self-hosted GitHub to Gitea mirroring service"; description = "Gitea Mirror - Self-hosted GitHub to Gitea mirroring service";
nixConfig = {
extra-substituters = [
"https://nix-community.cachix.org"
];
extra-trusted-public-keys = [
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
};
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
bun2nix = {
url = "github:nix-community/bun2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { self, nixpkgs, flake-utils }: outputs = { self, nixpkgs, flake-utils, bun2nix }:
let let
forEachSystem = flake-utils.lib.eachDefaultSystem; forEachSystem = flake-utils.lib.eachDefaultSystem;
in in
(forEachSystem (system: (forEachSystem (system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
b2n = bun2nix.packages.${system}.default;
# Build the application # Build the application
gitea-mirror = pkgs.stdenv.mkDerivation { gitea-mirror = pkgs.stdenv.mkDerivation {
@@ -21,8 +35,9 @@
src = ./.; src = ./.;
nativeBuildInputs = with pkgs; [ nativeBuildInputs = [
bun pkgs.bun
b2n.hook
]; ];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
@@ -30,21 +45,40 @@
openssl openssl
]; ];
configurePhase = '' bunDeps = b2n.fetchBunDeps {
export HOME=$TMPDIR bunNix = ./bun.nix;
export BUN_INSTALL=$TMPDIR/.bun };
export PATH=$BUN_INSTALL/bin:$PATH
''; # Let the bun2nix hook handle dependency installation via the
# pre-fetched cache, but skip its default build/check/install
# phases since we have custom ones.
dontUseBunBuild = true;
dontUseBunCheck = true;
dontUseBunInstall = true;
buildPhase = '' buildPhase = ''
# Install dependencies runHook preBuild
bun install --frozen-lockfile --no-progress export HOME=$TMPDIR
# Build the application # The bun2nix cache is in the read-only Nix store, but bunx/astro
# may try to write to it at build time. Copy the cache to a
# writable location.
if [ -n "$BUN_INSTALL_CACHE_DIR" ] && [ -d "$BUN_INSTALL_CACHE_DIR" ]; then
WRITABLE_CACHE="$TMPDIR/bun-cache"
cp -rL "$BUN_INSTALL_CACHE_DIR" "$WRITABLE_CACHE" 2>/dev/null || true
chmod -R u+w "$WRITABLE_CACHE" 2>/dev/null || true
export BUN_INSTALL_CACHE_DIR="$WRITABLE_CACHE"
fi
# Build the Astro application
bun run build bun run build
runHook postBuild
''; '';
installPhase = '' installPhase = ''
runHook preInstall
mkdir -p $out/lib/gitea-mirror mkdir -p $out/lib/gitea-mirror
mkdir -p $out/bin mkdir -p $out/bin
@@ -82,7 +116,18 @@ export MIRROR_PULL_REQUEST_CONCURRENCY=''${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
# Create data directory # Create data directory
mkdir -p "$DATA_DIR" mkdir -p "$DATA_DIR"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR/../lib/gitea-mirror" APP_DIR="$SCRIPT_DIR/../lib/gitea-mirror"
# The app uses process.cwd()/data for the database, but the Nix store
# is read-only. Create a writable working directory with symlinks to
# the app files and a real data directory.
WORK_DIR="$DATA_DIR/.workdir"
mkdir -p "$WORK_DIR"
for item in dist node_modules scripts src drizzle package.json tsconfig.json; do
ln -sfn "$APP_DIR/$item" "$WORK_DIR/$item"
done
ln -sfn "$DATA_DIR" "$WORK_DIR/data"
cd "$WORK_DIR"
# === AUTO-GENERATE SECRETS === # === AUTO-GENERATE SECRETS ===
BETTER_AUTH_SECRET_FILE="$DATA_DIR/.better_auth_secret" BETTER_AUTH_SECRET_FILE="$DATA_DIR/.better_auth_secret"
@@ -185,6 +230,8 @@ cd "$SCRIPT_DIR/../lib/gitea-mirror"
exec ${pkgs.bun}/bin/bun scripts/manage-db.ts "$@" exec ${pkgs.bun}/bin/bun scripts/manage-db.ts "$@"
EOF EOF
chmod +x $out/bin/gitea-mirror-db chmod +x $out/bin/gitea-mirror-db
runHook postInstall
''; '';
meta = with pkgs.lib; { meta = with pkgs.lib; {
@@ -209,6 +256,7 @@ EOF
bun bun
sqlite sqlite
openssl openssl
b2n
]; ];
shellHook = '' shellHook = ''
@@ -219,6 +267,10 @@ EOF
echo " bun run dev # Start development server" echo " bun run dev # Start development server"
echo " bun run build # Build for production" echo " bun run build # Build for production"
echo "" echo ""
echo "Nix packaging:"
echo " bun2nix -o bun.nix # Regenerate bun.nix after dependency changes"
echo " nix build # Build the package"
echo ""
echo "Database:" echo "Database:"
echo " bun run manage-db init # Initialize database" echo " bun run manage-db init # Initialize database"
echo " bun run db:studio # Open Drizzle Studio" echo " bun run db:studio # Open Drizzle Studio"

View File

@@ -75,8 +75,8 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"astro": "^5.17.3", "astro": "^5.17.3",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"buffer": "^6.0.3",
"better-auth": "1.4.19", "better-auth": "1.4.19",
"buffer": "^6.0.3",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -86,6 +86,7 @@
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"nanoid": "^3.3.11",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",