🌳
pt0/deployF/nixF/nixBuildNextImgAI.mts
1import fs from 'fs'
2import * as _ from 'lodash-es'
24import type { absFileDirPath } from '../../ptDirF.mts'
27const nixExprDir = `${ptTmpDir}/nix-build` as absFileDirPath
28const pnpmDepsHashCachePath = `${ptTmpDir}/nix-pnpm-deps-hash.txt` as absFileDirPath
30const ensureLocalDockerAuth = async ({dockreg_host, dockLanHost}: {dockreg_host: string, dockLanHost?: string}) => {
31 const dockConfigPath = `${envHome}/.docker/config.json` as absFileDirPath
32 const dockerConfigJson = JSON.parse(await read1File(dockConfigPath))
33 const authH = getReqJsonSecret(regCredSecName({dockreg_host}) + '.json' as secretNameType)
34 _.set(dockerConfigJson, ['auths', dockreg_host], authH)
35 if (dockLanHost) _.set(dockerConfigJson, ['auths', dockLanHost], authH)
36 dockerConfigJson.credsStore = ''
37 await write1File(dockConfigPath, JSON.stringify(dockerConfigJson, null, 2))
40const computeNixContentHash = ({nixExprContent, git_sha}: {nixExprContent: string, git_sha: string}) => {
41 const stripped = nixExprContent.replace(/outputHash\s*=\s*"[^"]*"/, 'outputHash=""')
42 return calcReqHash([stripped, git_sha], 'sha256').substring(0, 12)
45const getCachedPnpmDepsHash = async () => {
46 if (await fileExists(pnpmDepsHashCachePath)) {
47 const cached = JSON.parse(await read1File(pnpmDepsHashCachePath))
48 const currentLockHash = calcReqHash(await read1File(`${ptDir}/pnpm-lock.yaml` as absFileDirPath), 'sha256').substring(0, 16)
49 if (cached.lockHash === currentLockHash) return cached.pnpmDepsHash as string | null
50 }
51 return null
54const cachePnpmDepsHash = async (pnpmDepsHash: string) => {
55 const currentLockHash = calcReqHash(await read1File(`${ptDir}/pnpm-lock.yaml` as absFileDirPath), 'sha256').substring(0, 16)
57 await write1File(pnpmDepsHashCachePath, JSON.stringify({pnpmDepsHash, lockHash: currentLockHash}))
60const generateNixExpr = ({srcFilterPaths, appPath, svcPortNo, appPkgName, envJsonPath}: {srcFilterPaths: string[], appPath: string, svcPortNo: number, appPkgName: string, envJsonPath: string}) => {
61 const filterPaths = srcFilterPaths.map((p: string) => ` "${p}"`).join('\n')
63 return `{ pkgs ? import <nixpkgs> { system = "x86_64-linux"; } }:
64let
65 nodejs = pkgs.nodejs_24;
66 pnpm = pkgs.pnpm_10;
68 srcFiltered = builtins.path {
69 path = ${ptDir};
70 name = "monor-src";
71 filter = path: type:
72 let
73 relPath = pkgs.lib.removePrefix "${ptDir}/" (builtins.toString path);
74 isDir = type == "directory";
75 allowedFiles = [
76${filterPaths}
77 ];
78 isAllowedFile = builtins.elem relPath allowedFiles;
79 isParentOfAllowed = isDir && (builtins.any (f: pkgs.lib.hasPrefix (relPath + "/") f) allowedFiles);
80 in isAllowedFile || isParentOfAllowed;
81 };
83 dockerEnv = builtins.fromJSON (builtins.readFile ${envJsonPath});
85 nativeDeps = [
86 nodejs pnpm pkgs.python3 pkgs.pkg-config pkgs.cacert
87 pkgs.vips pkgs.cairo pkgs.pango pkgs.libjpeg pkgs.giflib pkgs.librsvg pkgs.pixman
88 pkgs.gnumake pkgs.gcc
89 ];
91 pkgConfigPath = builtins.concatStringsSep ":" [
92 "\${pkgs.cairo.dev}/lib/pkgconfig"
93 "\${pkgs.pango.dev}/lib/pkgconfig"
94 "\${pkgs.pixman}/lib/pkgconfig"
95 "\${pkgs.libjpeg.dev}/lib/pkgconfig"
96 "\${pkgs.giflib}/lib/pkgconfig"
97 "\${pkgs.librsvg.dev}/lib/pkgconfig"
98 "\${pkgs.glib.dev}/lib/pkgconfig"
99 "\${pkgs.freetype.dev}/lib/pkgconfig"
100 "\${pkgs.libpng.dev}/lib/pkgconfig"
101 "\${pkgs.harfbuzz.dev}/lib/pkgconfig"
102 ];
104 # phase 1: pnpm install with network access (__noChroot)
105 # pnpm install output is non-deterministic (timestamps, ordering) so fixed-output hashing doesn't work
106 # __noChroot allows network in a regular derivation when using --impure
107 pnpmDeps = pkgs.stdenv.mkDerivation {
108 name = "${appPkgName}-pnpm-deps";
109 src = srcFiltered;
110 nativeBuildInputs = nativeDeps;
111 __noChroot = true;
113 buildPhase = ''
114 export HOME=$TMPDIR
115 export SSL_CERT_FILE=\${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
116 export COREPACK_ENABLE_STRICT=0
117 export COREPACK_ENABLE_AUTO_PIN=0
118 export PATH="\${pnpm}/bin:$PATH"
119 export PKG_CONFIG_PATH="${"$"}{pkgConfigPath}:$PKG_CONFIG_PATH"
120 pnpm install --frozen-lockfile --filter ${appPkgName}...
121 '';
122 installPhase = ''
123 cp -r . $out
124 '';
125 };
127 # phase 2: regular derivation for next build — no network, no output hash
128 # non-deterministic output (webpack chunk hashes, timestamps) is fine here
129 builtApp = pkgs.stdenv.mkDerivation {
130 name = "${appPkgName}-built";
131 src = pnpmDeps;
132 nativeBuildInputs = nativeDeps;
134 configurePhase = ''
135 export HOME=$TMPDIR
136 export COREPACK_ENABLE_STRICT=0
137 export COREPACK_ENABLE_AUTO_PIN=0
138 export PATH="\${pnpm}/bin:$PATH"
139 export PKG_CONFIG_PATH="${"$"}{pkgConfigPath}:$PKG_CONFIG_PATH"
140 chmod -R u+w .
141 '';
142 buildPhase = ''
143 cd ${appPath}
144 export ISNEXTBUILD=1
145 export NODE_ENV=production
146 export NODE_OPTIONS="--no-warnings=ExperimentalWarning --max-old-space-size=${getAppCfg().nextBuildHeapMb || 2048}"
147 export NEXT_TELEMETRY_DISABLED=1
148 npx next build
149 cd $NIX_BUILD_TOP/$sourceRoot
150 '';
151 installPhase = ''
152 mkdir -p $out/approot
153 cp -r . $out/approot/
154 '';
155 };
157in pkgs.dockerTools.buildLayeredImage {
158 name = "nextjs-nix";
159 tag = "latest";
160 contents = [ nodejs ];
161 fakeRootCommands = ''
162 mkdir -p ./approot
163 cp -a \${builtApp}/approot/* ./approot/
164 mkdir -p ./approot/data
165 mkdir -p ./tmp
166 '';
167 config = {
168 Env = dockerEnv;
169 WorkingDir = "/approot/${appPath}";
170 ExposedPorts = { "${svcPortNo}/tcp" = {}; };
171 Cmd = [ "\${nodejs}/bin/node" "--enable-source-maps" "runnext.mjs" ];
172 };
177const runNixBuild = async ({nixExprPath}: {nixExprPath: string}) => {
178 const cmd = `nix build --impure --file ${nixExprPath} --no-link --print-out-paths`
179 console.log(cmd)
180 const ret = await liveSpawn({cmd: cmd + ' 2>&1', isQuiet: false, timeoutAfterSec: 1800})
181 if (!ret.isSuccess) {
182 console.error('nix build failed. output:', ((ret.stderr || '') + (ret.stdout || '')).slice(-2000))
183 throw new Error('nix build failed')
184 }
185 const nixStorePath = (ret.stdout || '').trim().split('\n').filter(l => l.startsWith('/nix/store/')).pop()
186 assertDefined(nixStorePath)
187 return nixStorePath
190export const nixBuildNextImg = async ({action, dockerEnv, git_sha, afterYarnInstallDockPathsA}: {action: string, dockerEnv: Record<string, string>, git_sha: string, afterYarnInstallDockPathsA: absFileDirPath[]}) => {
191 const {appPath, name, dockreg_host, dockLanHost} = getReqNextDeployCtx() as {appPath: string, name: string, dockreg_host: string, dockLanHost?: string}
192 const svcPortNo = getAppCfg()?.svcPortNo || 3000
193 const {dockName} = getDockBuildCtx()
195 const {pathsThatDontImportDevA, pathsThatImportDevDecA} = await filterDevOnlyPaths(afterYarnInstallDockPathsA)
196 const filteredPaths = [...pathsThatDontImportDevA, ...pathsThatImportDevDecA.map(d => d.path)]
198 const pkgJsonPtPathsA = await getPkgJsonPtPathsA({appPath})
199 const srcFilterPaths = _.uniq([...filteredPaths, ...pkgJsonPtPathsA, ...globalDependPaths]) as string[]
201 const appPkgName: string = JSON.parse(await read1File(`${ptDir}/${appPath}/package.json` as absFileDirPath)).name
204 fs.mkdirSync(nixExprDir, {recursive: true})
206 const envJsonPath = `${nixExprDir}/docker-env.json` as absFileDirPath
207 await write1File(envJsonPath, JSON.stringify(_.map(dockerEnv, (v, k) => `${k}=${v}`)))
209 const nixExpr = generateNixExpr({srcFilterPaths, appPath, svcPortNo, appPkgName, envJsonPath})
210 const contentHash = computeNixContentHash({nixExprContent: nixExpr, git_sha})
211 const syntheticDfContent = `nix:${contentHash}`
213 const {repo_name_tag, push_repo_name_tag} = await get2RepoNameTag({git_sha, dockerfileContent: syntheticDfContent})
215 if (await doesDockImg2Exist({git_sha, dockerfileContent: syntheticDfContent, reqDockPush: true})) {
216 console.log(chalkGreen(`nix: image exists ${repo_name_tag}`))
217 return [mkNixDockFile({repo_name_tag})]
218 }
220 const nixExprPath = `${nixExprDir}/nextjs-image.nix` as absFileDirPath
221 await write1File(nixExprPath, nixExpr)
223 console.log(`nix: building ${repo_name_tag}`)
225 const tarPath = await runNixBuild({nixExprPath})
226 console.log(`nix: loading image...`)
227 await ensureLocalDockerAuth({dockreg_host, dockLanHost})
228 await liveSpawnThrow({cmd: `${dockName} load < ${tarPath}`, isQuiet: true})
229 await liveSpawnThrow({cmd: `${dockName} tag nextjs-nix:latest ${push_repo_name_tag}`, isQuiet: true})
231 console.log(`nix: pushing ${push_repo_name_tag}`)
232 await liveSpawnThrow({cmd: `${dockName} push ${push_repo_name_tag}`, isQuiet: false, timeoutAfterSec: 900})
234 console.log(chalkGreen(`nix: image pushed ${repo_name_tag}`))
236 return [mkNixDockFile({repo_name_tag})]
239const mkNixDockFile = ({repo_name_tag}: {repo_name_tag: string}) => ({
240 isDockFile: true,
241 getRnt: async () => ({repo_name_tag, getRnt: async function() { return this }}),
242})