@@ -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
2829const 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.
294339func 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