unjs / nitro

Next Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.
https://nitro.unjs.io
MIT License
5.89k stars 496 forks source link

Support reproducible builds using `SOURCE_DATE_EPOCH` #2645

Open peterhirn opened 1 month ago

peterhirn commented 1 month ago

Describe the feature

I'd like to create reproducible builds using a framework based on Nitro.

I discovered three instances where Nitro injects non-reproducible timestamps into the output:

Using kaniko --reproducible I was able to create a fully reproducible build (ie. identical container sha256) after applying this patch with pnpm patch

patches/nitropack.patch

diff --git a/dist/nitro.mjs b/dist/nitro.mjs
index d54f501a2e63b68520868b8a40ec923cb73122ac..d00ac0d99091306fb3f9f6932dcd830bae60dcc9 100644
--- a/dist/nitro.mjs
+++ b/dist/nitro.mjs
@@ -1094,7 +1094,7 @@ function publicAssets(nitro) {
             type: nitro._prerenderMeta?.[assetId]?.contentType || mimeType,
             encoding,
             etag,
-            mtime: stat.mtime.toJSON(),
+            mtime: (process.env.SOURCE_DATE_EPOCH ? new Date(process.env.SOURCE_DATE_EPOCH * 1000) : stat.mtime).toJSON(),
             size: stat.size,
             path: relative(nitro.options.output.serverDir, fullPath),
             data: nitro.options.serveStatic === "inline" ? assetData.toString("base64") : void 0
@@ -1209,7 +1209,10 @@ function serverAssets(nitro) {
               type += "; charset=utf-8";
             }
             const etag = createEtag(await promises.readFile(fsPath));
-            const mtime = await promises.stat(fsPath).then((s) => s.mtime.toJSON());
+            const mtime =
+              process.env.SOURCE_DATE_EPOCH
+                  ? new Date(process.env.SOURCE_DATE_EPOCH * 1000).toJSON()
+                  : await promises.stat(fsPath).then((s) => s.mtime.toJSON());
             assets[id].meta = { type, etag, mtime };
           }
         }
@@ -2638,7 +2641,7 @@ async function _build(nitro, rollupConfig) {
   }
   const buildInfoPath = resolve(nitro.options.output.dir, "nitro.json");
   const buildInfo = {
-    date: (/* @__PURE__ */ new Date()).toJSON(),
+    date: (/* @__PURE__ */ process.env.SOURCE_DATE_EPOCH ? new Date(process.env.SOURCE_DATE_EPOCH * 1000) : new Date()).toJSON(),
     preset: nitro.options.preset,
     framework: nitro.options.framework,
     versions: {

This uses the recommended environment variable SOURCE_DATE_EPOCH, see https://reproducible-builds.org/docs/source-date-epoch/

Additional information

Notes

Initially I tried to use libfaketime with a fixed timestamp. This did work for nitro.json (ie. new Date()), but Node.js stats.mtime somehow always returns the current time, even though other cli tools returned the fake time (eg. ls, stat, date). Adding native support for SOURCE_DATE_EPOCH seems to be the preferred solution anyways: https://reproducible-builds.org/docs/timestamps/