Skip to content

Commit 3b8dcba

Browse files
committed
cmd/rofl/build: Improve reproducible builds with squashfs-tools
1 parent 085bb7b commit 3b8dcba

File tree

1 file changed

+124
-9
lines changed

1 file changed

+124
-9
lines changed

cmd/rofl/build/artifacts.go

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/oasisprotocol/oasis-core/go/common/crypto/hash"
2424

2525
"github.com/oasisprotocol/cli/build/env"
26+
"github.com/oasisprotocol/cli/cmd/common"
2627
)
2728

2829
const artifactCacheDir = "build_cache"
@@ -287,29 +288,143 @@ func ensureBinaryExists(buildEnv env.ExecEnv, name, pkg string) error {
287288
return nil
288289
}
289290

291+
// cleanEnvForReproducibility filters out locale and timestamp environment variables and sets
292+
// consistent values for reproducible builds.
293+
func cleanEnvForReproducibility() []string {
294+
env := []string{}
295+
for _, e := range os.Environ() {
296+
if !strings.HasPrefix(e, "LC_") && !strings.HasPrefix(e, "LANG=") && !strings.HasPrefix(e, "TZ=") && !strings.HasPrefix(e, "SOURCE_DATE_EPOCH=") {
297+
env = append(env, e)
298+
}
299+
}
300+
return append(env, "LC_ALL=C", "TZ=UTC", "SOURCE_DATE_EPOCH=0")
301+
}
302+
303+
// checkSquashfsVersion checks if the squashfs-tools version is 4.5.x and warns if not.
304+
func checkSquashfsVersion(buildEnv env.ExecEnv, sqfstarBin string) error {
305+
versionCmd := exec.Command(sqfstarBin, "-version")
306+
var versionOut strings.Builder
307+
versionCmd.Stdout = &versionOut
308+
versionCmd.Stderr = &versionOut
309+
if err := buildEnv.WrapCommand(versionCmd); err != nil {
310+
return err
311+
}
312+
313+
// Ignore errors from running the version command, as some versions may not support -version flag.
314+
_ = versionCmd.Run()
315+
316+
output := versionOut.String()
317+
// Look for version pattern like "mksquashfs version 4.5" or "sqfstar version 4.5"
318+
if strings.Contains(output, "version 4.5") {
319+
return nil
320+
}
321+
322+
// Version is not 4.5.x, show warning.
323+
fmt.Println("WARNING: squashfs-tools version 4.5.x is the officially supported version.")
324+
fmt.Println(" Builds with other versions might not be reproducible.")
325+
fmt.Printf(" Detected version output: %s\n", strings.TrimSpace(output))
326+
327+
if !common.IsForce() {
328+
return fmt.Errorf("unsupported squashfs-tools version (use --force to proceed anyway)")
329+
}
330+
331+
fmt.Println(" Proceeding anyway due to --force flag.")
332+
return nil
333+
}
334+
290335
// createSquashFs creates a squashfs filesystem in the given file using directory dir to populate
291336
// it.
292337
//
293338
// Returns the size of the created filesystem image in bytes.
294339
func createSquashFs(buildEnv env.ExecEnv, fn, dir string) (int64, error) {
295-
const mkSquashFsBin = "mksquashfs"
296-
if err := ensureBinaryExists(buildEnv, mkSquashFsBin, "squashfs-tools"); err != nil {
340+
const sqfstarBin = "sqfstar"
341+
if err := ensureBinaryExists(buildEnv, sqfstarBin, "squashfs-tools"); err != nil {
342+
return 0, err
343+
}
344+
if err := ensureBinaryExists(buildEnv, "fakeroot", "fakeroot"); err != nil {
297345
return 0, err
298346
}
299347

300-
// Execute mksquashfs.
348+
// Check squashfs-tools version.
349+
if err := checkSquashfsVersion(buildEnv, sqfstarBin); err != nil {
350+
return 0, err
351+
}
352+
353+
// Create reproducible tar archive.
354+
tarPath := fn + ".tar"
355+
fmt.Printf("Creating tar archive: %s\n", tarPath)
356+
//nolint:gosec // tarPath is constructed internally, not from user input.
357+
tarCmd := exec.Command(
358+
"tar",
359+
"--create",
360+
"--file="+tarPath,
361+
"--format=pax",
362+
"--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime",
363+
"--sort=name",
364+
"--mtime=@0",
365+
"--owner=0",
366+
"--group=0",
367+
"--numeric-owner",
368+
"--mode=a-s",
369+
".",
370+
)
371+
tarCmd.Dir = dir
372+
tarCmd.Env = cleanEnvForReproducibility()
373+
374+
var tarOut strings.Builder
375+
tarCmd.Stderr = &tarOut
376+
tarCmd.Stdout = &tarOut
377+
if err := buildEnv.WrapCommand(tarCmd); err != nil {
378+
return 0, err
379+
}
380+
if err := tarCmd.Run(); err != nil {
381+
return 0, fmt.Errorf("failed to create tar archive: %w\n%s", err, tarOut.String())
382+
}
383+
384+
// Compute and print tar archive hash for verification.
385+
tarFile, err := os.Open(tarPath)
386+
if err != nil {
387+
return 0, fmt.Errorf("failed to open tar archive for hashing: %w", err)
388+
}
389+
tarHasher := sha256.New()
390+
if _, err := io.Copy(tarHasher, tarFile); err != nil {
391+
tarFile.Close()
392+
return 0, fmt.Errorf("failed to hash tar archive: %w", err)
393+
}
394+
tarFile.Close()
395+
tarHash := hex.EncodeToString(tarHasher.Sum(nil))
396+
fmt.Printf("TAR archive SHA256: %s\n", tarHash)
397+
398+
// Convert tar to squashfs using sqfstar under fakeroot.
301399
cmd := exec.Command(
302-
mkSquashFsBin,
303-
dir,
400+
"fakeroot",
401+
"--",
402+
sqfstarBin,
304403
fn,
404+
"--reproducible",
305405
"-comp", "gzip",
406+
"-b", "1M",
407+
"-processors", "1",
306408
"-noappend",
307-
"-mkfs-time", "1234",
308-
"-all-time", "1234",
309-
"-root-time", "1234",
409+
"-mkfs-time", "0",
410+
"-all-time", "0",
411+
"-nopad",
310412
"-all-root",
311-
"-reproducible",
413+
"-force-uid", "0",
414+
"-force-gid", "0",
312415
)
416+
417+
cmd.Env = cleanEnvForReproducibility()
418+
419+
// Open tar file and pipe it to sqfstar's stdin.
420+
tarFile, err = os.Open(tarPath)
421+
if err != nil {
422+
return 0, fmt.Errorf("failed to open tar archive: %w", err)
423+
}
424+
defer tarFile.Close()
425+
defer os.Remove(tarPath)
426+
427+
cmd.Stdin = tarFile
313428
var out strings.Builder
314429
cmd.Stderr = &out
315430
cmd.Stdout = &out

0 commit comments

Comments
 (0)