diff --git a/dist/yarn-plugin-nixify.js b/dist/yarn-plugin-nixify.js index f765623..0bbde59 100644 --- a/dist/yarn-plugin-nixify.js +++ b/dist/yarn-plugin-nixify.js @@ -1 +1 @@ -module.exports={name:"yarn-plugin-nixify",factory:function(e){return(()=>{"use strict";var n={216:(e,n,t)=>{t.r(n),t.d(n,{default:()=>v});var r,i,o,a,s,c,l,p,u,d,h,f,y=t(42),b=t(688),m=t(798),g=t(966);function x(e,n,t,r){t&&Object.defineProperty(e,n,{enumerable:t.enumerable,configurable:t.configurable,writable:t.writable,value:t.initializer?t.initializer.call(r):void 0})}function P(e,n,t,r,i){var o={};return Object.keys(r).forEach((function(e){o[e]=r[e]})),o.enumerable=!!o.enumerable,o.configurable=!!o.configurable,("value"in o||o.initializer)&&(o.writable=!0),o=t.slice().reverse().reduce((function(t,r){return r(e,n,t)||t}),o),i&&void 0!==o.initializer&&(o.value=o.initializer?o.initializer.call(i):void 0,o.initializer=void 0),void 0===o.initializer&&(Object.defineProperty(e,n,o),o=null),o}const v={commands:[(r=y.Command.Path("nixify"),P((i=class extends y.Command{async execute(){const e=await g.Configuration.find(this.context.cwd,this.context.plugins),{project:n}=await g.Project.find(e,this.context.cwd);return(await g.StreamReport.start({configuration:e,stdout:this.context.stdout},async e=>(async(e,n)=>{const{configuration:t,cwd:r}=e,i=t.get("yarnPath"),o=t.get("lockfileFilename");if("pnp"!==t.get("nodeLinker"))return void n.reportError(0,"Only 'pnp' is currently supported for the 'nodeLinker' setting.");const a=b.ppath.relative(r,i);if(a.startsWith("."))return void n.reportError(0,`The Yarn path ${i} is outside the project directory - it cannot be reached by the Nix build`);if(!b.xfs.existsSync(b.ppath.join(r,o))||!b.xfs.existsSync((0,m.getPnpPath)(e).main))return void n.reportError(0,`The project in ${r}/package.json doesn't seem to have been installed - running an install there might help`);const s=new Set(["package.json",o,a]);for(const e of t.sources.values())e.startsWith("<")||s.add(e);const c=b.ppath.join(r,".yarn/plugins");if(b.xfs.existsSync(c))for(const e of b.xfs.readdirSync(c))s.add(b.ppath.join(c,e));const l=new Set;for(const e of s){const t=b.ppath.relative(r,e);if(t.startsWith("..")){n.reportWarning(0,`The path ${e} was ignored, because it cannot be reached by the Nix build`);continue}l.add("regular:"+t);let i=b.ppath.dirname(t);for(;"."!==i;)l.add("directory:"+i),i=b.ppath.dirname(i)}const p=t.get("cacheFolder"),u=(await g.execUtils.execvp("nix-hash",["--type","sha256","--base32",p],{cwd:r,encoding:"utf8",strict:!0})).stdout.trim(),d=e.topLevelWorkspace.manifest.name,h=d?g.structUtils.stringifyIdent(d):"workspace",f="{ lib, bash, nodejs, perl, stdenv, writeText }:\n\nwith lib;\n\nlet\n\n # Variables provided by the generator.\n project-name = @@PROJECT_NAME@@;\n offline-cache-hash = @@OFFLINE_CACHE_HASH@@;\n yarn-path = @@YARN_PATH@@;\n yarn-closure-entries = @@YARN_CLOSURE_ENTRIES@@;\n\n # Defines the shell alias to run Yarn.\n yarn-alias = ''\n yarn() {\n CI=1 \"$NIX_YARN_PATH\" \"$@\"\n }\n '';\n\n # Directory of just the files needed to run Yarn.\n yarn-closure = cleanSourceWith {\n src = ./.;\n filter = let\n srcStr = toString ./.;\n srcRel = removePrefix \"${srcStr}/\";\n in path: type:\n elem \"${type}:${srcRel path}\" yarn-closure-entries;\n };\n\n # Build just the offline cache for the project.\n offline-cache = stdenv.mkDerivation {\n name = \"${project-name}-offline-cache\";\n buildInputs = [ nodejs ];\n builder = writeText \"builder.sh\" ''\n source $stdenv/setup\n cd ${yarn-closure}\n\n # Yarn may need a writable home directory for the global cache mirror.\n # TODO: Can't disable the mirror, because it changes cache filenames.\n export HOME=\"$TEMP\"\n\n # Setup to environment so we can run Yarn.\n export NIX_YARN_PATH=\"$PWD/${yarn-path}\"\n ${yarn-alias}\n\n # Invoke a plugin internal command to build the cache.\n yarn nixify build-cache $out\n '';\n outputHashMode = \"recursive\";\n outputHashAlgo = \"sha256\";\n outputHash = offline-cache-hash;\n };\n\nin stdenv.mkDerivation {\n name = project-name;\n src = ./.;\n\n # Tell node-gyp to use the provided Node.js headers for native code builds.\n npm_config_nodedir = nodejs;\n # Tell node-pre-gyp to never fetch binaries / always build from source.\n npm_config_build_from_source = \"true\";\n\n # Make sure the build uses the right Node.js version everywhere.\n buildInputs = [ nodejs ];\n\n # Define the Yarn alias in the build environment.\n postHook = yarn-alias;\n\n configurePhase = ''\n runHook preConfigure\n\n # Move the entire project to the output directory.\n # TODO: Would rather do this in 'installPhase',\n # but '.pnp.js' is generated with relative paths.\n mkdir -p $out/libexec $out/bin\n mv $PWD \"$out/libexec/$sourceRoot\"\n cd \"$out/libexec/$sourceRoot\"\n\n # Store the absolute path to Yarn for the 'yarn' alias.\n export NIX_YARN_PATH=\"$PWD/${yarn-path}\"\n\n # Point Yarn to the offline cache built separately.\n yarn config set cacheFolder '${offline-cache}'\n\n # Run normal Yarn install to complete dependency installation.\n yarn install --immutable --immutable-cache\n\n runHook postConfigure\n '';\n\n buildPhase = ''\n runHook preBuild\n runHook postBuild\n '';\n\n installPhase = ''\n runHook preInstall\n\n # Invoke a plugin internal command to setup binaries.\n yarn nixify install-bin $out/bin\n\n runHook postInstall\n '';\n\n passthru = {\n inherit yarn-closure offline-cache;\n };\n}\n".replace("@@PROJECT_NAME@@",JSON.stringify(h)).replace("@@OFFLINE_CACHE_HASH@@",JSON.stringify(u)).replace("@@YARN_PATH@@",JSON.stringify(a)).replace("@@YARN_CLOSURE_ENTRIES@@","[ "+[...l].map(e=>JSON.stringify(e)).join(" ")+" ]"),y=b.ppath.join(r,"yarn-project.nix");b.xfs.writeFileSync(y,f),n.reportInfo(0,"Wrote: "+y);const x=b.ppath.join(r,"default.nix");b.xfs.existsSync(x)||(b.xfs.writeFileSync(x,"{ pkgs ? import {} }:\n\npkgs.callPackage ./yarn-project.nix {}\n"),n.reportInfo(0,"Wrote: "+x))})(n,e))).exitCode()}}).prototype,"execute",[r],Object.getOwnPropertyDescriptor(i.prototype,"execute"),i.prototype),i),(o=y.Command.String(),a=y.Command.Path("nixify","build-cache"),l=class extends y.Command{constructor(...e){super(...e),x(this,"out",c,this)}async execute(){const e=await g.Configuration.find(this.context.cwd,this.context.plugins);e.use("",{cacheFolder:this.out},e.projectCwd,{overwrite:!0});const{project:n}=await g.Project.find(e,this.context.cwd),t=await g.Cache.find(e);return(await g.StreamReport.start({configuration:e,stdout:this.context.stdout},async e=>{await e.startTimerPromise("Resolution step",()=>n.resolveEverything({report:e,lockfileOnly:!0})),await e.startTimerPromise("Fetch step",()=>n.fetchEverything({cache:t,report:e}))})).exitCode()}},c=P((s=l).prototype,"out",[o],{configurable:!0,enumerable:!0,writable:!0,initializer:function(){return""}}),P(s.prototype,"execute",[a],Object.getOwnPropertyDescriptor(s.prototype,"execute"),s.prototype),s),(p=y.Command.String(),u=y.Command.Path("nixify","install-bin"),f=class extends y.Command{constructor(...e){super(...e),x(this,"binDir",h,this)}async execute(){const e=await g.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:t}=await g.Project.find(e,this.context.cwd);if(t){const e=b.npath.toPortablePath(this.binDir),r=(0,m.getPnpPath)(n).main;for(const[i,o]of t.manifest.bin){const t=b.ppath.join(e,i),a=b.ppath.join(n.cwd,b.npath.toPortablePath(o)),s="#!/bin/sh\nexec node -r '@@PNP_PATH@@' '@@SCRIPT_PATH@@' \"$@\"\n".replace("@@PNP_PATH@@",r).replace("@@SCRIPT_PATH@@",a);b.xfs.writeFileSync(t,s),b.xfs.chmodSync(t,493)}}}},h=P((d=f).prototype,"binDir",[p],{configurable:!0,enumerable:!0,writable:!0,initializer:function(){return""}}),P(d.prototype,"execute",[u],Object.getOwnPropertyDescriptor(d.prototype,"execute"),d.prototype),d)]}},966:n=>{n.exports=e("@yarnpkg/core")},688:n=>{n.exports=e("@yarnpkg/fslib")},798:n=>{n.exports=e("@yarnpkg/plugin-pnp")},42:n=>{n.exports=e("clipanion")}},t={};function r(e){if(t[e])return t[e].exports;var i=t[e]={exports:{}};return n[e](i,i.exports,r),i.exports}return r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r(216)})()}}; \ No newline at end of file +module.exports={name:"yarn-plugin-nixify",factory:function(e){return(()=>{"use strict";var n={216:(e,n,t)=>{t.r(n),t.d(n,{default:()=>x});var r,i,o,a,s,c,l,p,u,h,d=t(42),f=t(688),y=t(798),m=t(966);function b(e,n,t,r){t&&Object.defineProperty(e,n,{enumerable:t.enumerable,configurable:t.configurable,writable:t.writable,value:t.initializer?t.initializer.call(r):void 0})}function g(e,n,t,r,i){var o={};return Object.keys(r).forEach((function(e){o[e]=r[e]})),o.enumerable=!!o.enumerable,o.configurable=!!o.configurable,("value"in o||o.initializer)&&(o.writable=!0),o=t.slice().reverse().reduce((function(t,r){return r(e,n,t)||t}),o),i&&void 0!==o.initializer&&(o.value=o.initializer?o.initializer.call(i):void 0,o.initializer=void 0),void 0===o.initializer&&(Object.defineProperty(e,n,o),o=null),o}const x={commands:[(r=d.Command.String(),i=d.Command.Path("nixify","fetch-one"),s=class extends d.Command{constructor(...e){super(...e),b(this,"locatorHash",a,this)}async execute(){const e=await m.Configuration.find(this.context.cwd,this.context.plugins),{project:n}=await m.Project.find(e,this.context.cwd),t=await m.Cache.find(e),r=e.makeFetcher();return(await m.StreamReport.start({configuration:e,stdout:this.context.stdout},async e=>{const i=n.originalPackages.get(this.locatorHash);i?await r.fetch(i,{checksums:n.storedChecksums,project:n,cache:t,fetcher:r,report:e}):e.reportError(0,"Invalid locator hash")})).exitCode()}},a=g((o=s).prototype,"locatorHash",[r],{configurable:!0,enumerable:!0,writable:!0,initializer:function(){return""}}),g(o.prototype,"execute",[i],Object.getOwnPropertyDescriptor(o.prototype,"execute"),o.prototype),o),(c=d.Command.String(),l=d.Command.Path("nixify","install-bin"),h=class extends d.Command{constructor(...e){super(...e),b(this,"binDir",u,this)}async execute(){const e=await m.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:t}=await m.Project.find(e,this.context.cwd);if(t){const e=f.npath.toPortablePath(this.binDir),r=(0,y.getPnpPath)(n).main;for(const[i,o]of t.manifest.bin){const t=f.ppath.join(e,i),a=f.ppath.join(n.cwd,f.npath.toPortablePath(o)),s="#!/bin/sh\nexec node -r '@@PNP_PATH@@' '@@SCRIPT_PATH@@' \"$@\"\n".replace("@@PNP_PATH@@",r).replace("@@SCRIPT_PATH@@",a);f.xfs.writeFileSync(t,s),f.xfs.chmodSync(t,493)}}}},u=g((p=h).prototype,"binDir",[c],{configurable:!0,enumerable:!0,writable:!0,initializer:function(){return""}}),g(p.prototype,"execute",[l],Object.getOwnPropertyDescriptor(p.prototype,"execute"),p.prototype),p)],hooks:{afterAllInstalled:async(e,n)=>{!1!==n.persistProject&&await(async(e,n,t)=>{const{configuration:r,cwd:i}=e,o=r.get("yarnPath"),a=r.get("cacheFolder"),s=r.get("lockfileFilename");if("pnp"!==r.get("nodeLinker"))return void t.reportWarning(0,"Currently, yarn-plugin-nixify only supports 'pnp' for the 'nodeLinker' setting.");let c=f.ppath.relative(i,o);c.startsWith("../")&&(c=o,t.reportWarning(0,`The Yarn path ${o} is outside the project - it may not be reachable by the Nix build`));let l=f.ppath.relative(i,a);l.startsWith("../")&&(l=a,t.reportWarning(0,`The cache folder ${a} is outside the project - it may not be reachable by the Nix build`));const p=new Set(["package.json",s,c]);for(const e of r.sources.values())e.startsWith("<")||p.add(e);const u=f.ppath.join(i,".yarn/plugins");if(f.xfs.existsSync(u))for(const e of f.xfs.readdirSync(u))p.add(f.ppath.join(u,e));const h=new Set;for(const e of p){const n=f.ppath.relative(i,e);if(n.startsWith("../")){e!==c&&t.reportWarning(0,`The path ${e} is outside the project and was ignored - it may not be reachable in the Nix build`);continue}h.add("regular:"+n);let r=f.ppath.dirname(n);for(;"."!==r;)h.add("directory:"+r),r=f.ppath.dirname(r)}let d=[];const y=new Set(f.xfs.readdirSync(n.cwd));for(const t of e.storedPackages.values()){const r=e.storedChecksums.get(t.locatorHash);if(!r)continue;const i=n.getLocatorPath(t,r);if(!i)continue;const o=f.ppath.basename(i);if(!y.has(o))continue;const a=r.split("/").pop();d.push([`filename = ${JSON.stringify(o)};`,`sha512 = ${JSON.stringify(a)};`,`locator-hash = ${JSON.stringify(t.locatorHash)};`])}const b=e.topLevelWorkspace.manifest.name,g=b?m.structUtils.stringifyIdent(b):"workspace",x="# This file is generated by running \"yarn install\" inside your project.\n# Manual changes might be lost - proceed with caution!\n\n{ lib, bash, coreutils, nodejs, perl, stdenv, writeText }:\n\nwith lib;\n\nlet\n\n # Variables provided by the generator.\n project-name = @@PROJECT_NAME@@;\n cache-folder = @@CACHE_FOLDER@@;\n cache-entries = @@CACHE_ENTRIES@@;\n yarn-path = @@YARN_PATH@@;\n yarn-closure-entries = @@YARN_CLOSURE_ENTRIES@@;\n\n # Defines the shell alias to run Yarn.\n yarn-alias = ''\n yarn() {\n CI=1 node \"$NIX_YARN_PATH\" \"$@\"\n }\n '';\n\n # Directory of just the files needed to run Yarn.\n yarn-closure = cleanSourceWith {\n src = ./.;\n filter = let\n srcStr = toString ./.;\n srcRel = removePrefix \"${srcStr}/\";\n in path: type:\n elem \"${type}:${srcRel path}\" yarn-closure-entries;\n };\n\n # Fetch a single dependency.\n fetch = { filename, sha512, locator-hash }: stdenv.mkDerivation {\n name = replaceStrings [ \"@\" ] [ \"-\" ] filename;\n buildInputs = [ nodejs ];\n builder = builtins.toFile \"builder.sh\" ''\n source $stdenv/setup\n\n # Yarn may need a writable home directory for the global cache mirror.\n # TODO: Can't disable the mirror, because it changes cache filenames.\n export HOME=\"$PWD\"\n\n # Fetch into the build directory.\n export yarn_cache_folder=\"$PWD\"\n\n # Setup to environment so we can run Yarn.\n pushd '${yarn-closure}' > /dev/null\n\n export NIX_YARN_PATH=\"$(readlink -f '${yarn-path}')\"\n ${yarn-alias}\n\n # Invoke a plugin internal command to build the cache.\n yarn nixify fetch-one '${locator-hash}'\n\n popd > /dev/null\n\n # The cache file that was just fetched is our output.\n mv '${filename}' $out\n '';\n outputHashMode = \"flat\";\n outputHashAlgo = \"sha512\";\n outputHash = sha512;\n };\n\n # Shell snippet to collect all project dependencies.\n collect-cache = concatMapStrings (args: ''\n cp ${fetch args} './${args.filename}'\n '') cache-entries;\n\nin stdenv.mkDerivation {\n name = project-name;\n src = ./.;\n\n # Tell node-gyp to use the provided Node.js headers for native code builds.\n npm_config_nodedir = nodejs;\n # Tell node-pre-gyp to never fetch binaries / always build from source.\n npm_config_build_from_source = \"true\";\n\n # Make sure the build uses the right Node.js version everywhere.\n buildInputs = [ coreutils nodejs ];\n\n # Define the Yarn alias in the build environment.\n postHook = yarn-alias;\n\n configurePhase = ''\n runHook preConfigure\n\n # Copy over the Yarn cache.\n # TODO: Can we do without the copy somehow? Links don't work.\n rm -fr '${cache-folder}'\n mkdir -p '${cache-folder}'\n pushd '${cache-folder}' > /dev/null\n ${collect-cache}\n popd > /dev/null\n\n # Store the absolute path to Yarn for the 'yarn' alias.\n export NIX_YARN_PATH=\"$(readlink -f '${yarn-path}')\"\n\n # Run normal Yarn install to complete dependency installation.\n yarn install --immutable --immutable-cache\n\n runHook postConfigure\n '';\n\n buildPhase = ''\n runHook preBuild\n runHook postBuild\n '';\n\n installPhase = ''\n runHook preInstall\n\n mkdir -p $out/libexec $out/bin\n\n # Move the entire project to the output directory.\n mv $PWD \"$out/libexec/$sourceRoot\"\n cd \"$out/libexec/$sourceRoot\"\n\n # Update the path to Yarn.\n export NIX_YARN_PATH=\"$(readlink -f '${yarn-path}')\"\n\n # Invoke a plugin internal command to setup binaries.\n yarn nixify install-bin $out/bin\n\n runHook postInstall\n '';\n\n passthru = {\n inherit yarn-closure offline-cache;\n };\n}\n".replace("@@PROJECT_NAME@@",JSON.stringify(g)).replace("@@CACHE_FOLDER@@",JSON.stringify(l)).replace("@@CACHE_ENTRIES@@","[\n"+[...d].map(e=>` { ${e.join(" ")} }\n`).join("")+" ]").replace("@@YARN_PATH@@",JSON.stringify(c)).replace("@@YARN_CLOSURE_ENTRIES@@","[\n"+[...h].map(e=>` ${JSON.stringify(e)}\n`).join("")+" ]"),v=f.ppath.join(i,"yarn-project.nix");f.xfs.writeFileSync(v,x);const P=f.ppath.join(i,"default.nix");f.xfs.existsSync(P)||(f.xfs.writeFileSync(P,"# This is a minimal `default.nix` by yarn-plugin-nixify. You can customize it\n# as needed, it will not be overwritten by the plugin.\n\n{ pkgs ? import {} }:\n\npkgs.callPackage ./yarn-project.nix {}\n"),t.reportInfo(0,"A minimal default.nix was created. You may want to customize it."))})(e,n.cache,n.report)}}}},966:n=>{n.exports=e("@yarnpkg/core")},688:n=>{n.exports=e("@yarnpkg/fslib")},798:n=>{n.exports=e("@yarnpkg/plugin-pnp")},42:n=>{n.exports=e("clipanion")}},t={};function r(e){if(t[e])return t[e].exports;var i=t[e]={exports:{}};return n[e](i,i.exports,r),i.exports}return r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r(216)})()}}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index dde3644..b0346e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,11 +7,11 @@ import { CommandContext, Configuration, Hooks, + LocatorHash, Plugin, Project, Report, StreamReport, - execUtils, structUtils, } from "@yarnpkg/core"; @@ -20,7 +20,7 @@ import defaultExprTmpl from "./default.nix.in"; import projectExprTmpl from "./yarn-project.nix.in"; // Generator function that runs after `yarn install`. -const generate = async (project: Project, report: Report) => { +const generate = async (project: Project, cache: Cache, report: Report) => { const { configuration, cwd } = project; const yarnPathAbs = configuration.get(`yarnPath`); const cacheFolderAbs = configuration.get(`cacheFolder`); @@ -99,25 +99,25 @@ const generate = async (project: Project, report: Report) => { } } - // Build the Nix output-hash by hashing the Yarn cache folder. The - // derivation should build the exact same. - let cacheHash = ``; - try { - const hasherResult = await execUtils.execvp( - `nix-hash`, - [`--type`, `sha256`, `--base32`, cacheFolder], - { cwd, encoding: `utf8`, strict: true } - ); - cacheHash = hasherResult.stdout.trim(); - } catch (err) { - if (err.code === `ENOENT`) { - report.reportWarning( - 0, - `No Nix installation found - yarn-project.nix will not be updated` - ); - } else { - throw err; - } + // Build a list of cache entries so Nix can fetch them. + let cacheEntries = []; + const cacheFiles = new Set(xfs.readdirSync(cache.cwd)); + for (const pkg of project.storedPackages.values()) { + const checksum = project.storedChecksums.get(pkg.locatorHash); + if (!checksum) continue; + + const cachePath = cache.getLocatorPath(pkg, checksum); + if (!cachePath) continue; + + const filename = ppath.basename(cachePath); + if (!cacheFiles.has(filename)) continue; + + const sha512 = checksum.split(`/`).pop(); + cacheEntries.push([ + `filename = ${JSON.stringify(filename)};`, + `sha512 = ${JSON.stringify(sha512)};`, + `locator-hash = ${JSON.stringify(pkg.locatorHash)};`, + ]); } // Render the Nix expression. @@ -126,15 +126,22 @@ const generate = async (project: Project, report: Report) => { const projectExpr = projectExprTmpl .replace(`@@PROJECT_NAME@@`, JSON.stringify(projectName)) .replace(`@@CACHE_FOLDER@@`, JSON.stringify(cacheFolder)) - .replace(`@@CACHE_HASH@@`, JSON.stringify(cacheHash)) + .replace( + `@@CACHE_ENTRIES@@`, + `[\n` + + [...cacheEntries] + .map((entry) => ` { ${entry.join(` `)} }\n`) + .join(``) + + ` ]` + ) .replace(`@@YARN_PATH@@`, JSON.stringify(yarnPath)) .replace( `@@YARN_CLOSURE_ENTRIES@@`, - `[ ` + + `[\n` + [...yarnClosureEntries] - .map((entry) => JSON.stringify(entry)) - .join(` `) + - ` ]` + .map((entry) => ` ${JSON.stringify(entry)}\n`) + .join(``) + + ` ]` ); const projectExprPath = ppath.join(cwd, `yarn-project.nix` as Filename); xfs.writeFileSync(projectExprPath, projectExpr); @@ -150,40 +157,41 @@ const generate = async (project: Project, report: Report) => { } }; -// Internal command that does just the fetch part of `yarn install`. -// Used inside the Nix offline-cache derivation to build the cache. -class BuildCacheCommand extends Command { +// Internal command that fetches a single locator. +// Used from within Nix to build the cache for the project. +class FetchOneCommand extends Command { @Command.String() - out: string = ``; + locatorHash: string = ``; - @Command.Path(`nixify`, `build-cache`) + @Command.Path(`nixify`, `fetch-one`) async execute() { const configuration = await Configuration.find( this.context.cwd, this.context.plugins ); - - // Overwrite the cache directory to our output directory. - configuration.use( - ``, - { cacheFolder: this.out }, - configuration.projectCwd!, - { overwrite: true } - ); - const { project } = await Project.find(configuration, this.context.cwd); const cache = await Cache.find(configuration); - // Run resolution and fetch steps. + const fetcher = configuration.makeFetcher(); + const report = await StreamReport.start( { configuration, stdout: this.context.stdout }, async (report) => { - await report.startTimerPromise(`Resolution step`, () => - project.resolveEverything({ report, lockfileOnly: true }) - ); - await report.startTimerPromise(`Fetch step`, () => - project.fetchEverything({ cache, report }) + const pkg = project.originalPackages.get( + this.locatorHash as LocatorHash ); + if (!pkg) { + report.reportError(0, `Invalid locator hash`); + return; + } + + await fetcher.fetch(pkg, { + checksums: project.storedChecksums, + project, + cache, + fetcher, + report, + }); } ); @@ -228,11 +236,11 @@ class InstallBinCommand extends Command { } const plugin: Plugin = { - commands: [BuildCacheCommand, FetchLocatorCommand, InstallBinCommand], + commands: [FetchOneCommand, InstallBinCommand], hooks: { afterAllInstalled: async (project, opts) => { if (opts.persistProject !== false) { - await generate(project, opts.report); + await generate(project, opts.cache, opts.report); } }, }, diff --git a/src/yarn-project.nix.in b/src/yarn-project.nix.in index bef9021..cd8acdc 100644 --- a/src/yarn-project.nix.in +++ b/src/yarn-project.nix.in @@ -10,7 +10,7 @@ let # Variables provided by the generator. project-name = @@PROJECT_NAME@@; cache-folder = @@CACHE_FOLDER@@; - cache-hash = @@CACHE_HASH@@; + cache-entries = @@CACHE_ENTRIES@@; yarn-path = @@YARN_PATH@@; yarn-closure-entries = @@YARN_CLOSURE_ENTRIES@@; @@ -31,30 +31,44 @@ let elem "${type}:${srcRel path}" yarn-closure-entries; }; - # Build just the cache for the project. - offline-cache = stdenv.mkDerivation { - name = "${project-name}-offline-cache"; + # Fetch a single dependency. + fetch = { filename, sha512, locator-hash }: stdenv.mkDerivation { + name = replaceStrings [ "@" ] [ "-" ] filename; buildInputs = [ nodejs ]; - builder = writeText "builder.sh" '' + builder = builtins.toFile "builder.sh" '' source $stdenv/setup - cd '${yarn-closure}' # Yarn may need a writable home directory for the global cache mirror. # TODO: Can't disable the mirror, because it changes cache filenames. - export HOME="$TEMP" + export HOME="$PWD" + + # Fetch into the build directory. + export yarn_cache_folder="$PWD" # Setup to environment so we can run Yarn. + pushd '${yarn-closure}' > /dev/null + export NIX_YARN_PATH="$(readlink -f '${yarn-path}')" ${yarn-alias} # Invoke a plugin internal command to build the cache. - yarn nixify build-cache $out + yarn nixify fetch-one '${locator-hash}' + + popd > /dev/null + + # The cache file that was just fetched is our output. + mv '${filename}' $out ''; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; - outputHash = cache-hash; + outputHashMode = "flat"; + outputHashAlgo = "sha512"; + outputHash = sha512; }; + # Shell snippet to collect all project dependencies. + collect-cache = concatMapStrings (args: '' + cp ${fetch args} './${args.filename}' + '') cache-entries; + in stdenv.mkDerivation { name = project-name; src = ./.; @@ -74,11 +88,12 @@ in stdenv.mkDerivation { runHook preConfigure # Copy over the Yarn cache. - # TODO: Can we do without the copy somehow? + # TODO: Can we do without the copy somehow? Links don't work. rm -fr '${cache-folder}' - mkdir -p "$(dirname '${cache-folder}')" - cp -r '${offline-cache}' '${cache-folder}' - chmod -R u+w '${cache-folder}' + mkdir -p '${cache-folder}' + pushd '${cache-folder}' > /dev/null + ${collect-cache} + popd > /dev/null # Store the absolute path to Yarn for the 'yarn' alias. export NIX_YARN_PATH="$(readlink -f '${yarn-path}')"