@@ -137,18 +137,33 @@ func cleanEnvForReproducibility() []string {
137137 return append (env , "LC_ALL=C" , "TZ=UTC" , "SOURCE_DATE_EPOCH=0" )
138138}
139139
140- // ExtraFile represents a file to be injected into the rootfs.
141- type ExtraFile struct {
140+ // extraFile represents a file to be injected into the rootfs.
141+ type extraFile struct {
142142 HostPath string // Path on host filesystem
143143 TarPath string // Path inside the tar/rootfs (e.g., "init" or "etc/config")
144144 Mode os.FileMode // File mode (e.g., 0o755 for executables)
145145}
146146
147+ // normalizeHeader normalizes a tar header for reproducible builds.
148+ // Uses GNU format because sqfstar < 4.6 has a bug with PAX headers
149+ // that incorrectly strips pathname components in linkpath for symlinks.
150+ func normalizeHeader (header * tar.Header ) {
151+ header .Format = tar .FormatGNU
152+ header .Uid = 0
153+ header .Gid = 0
154+ header .Uname = ""
155+ header .Gname = ""
156+ header .ModTime = time .Unix (0 , 0 )
157+ header .AccessTime = time.Time {}
158+ header .ChangeTime = time.Time {}
159+ header .PAXRecords = nil
160+ }
161+
147162// createSquashFsFromTar creates a squashfs filesystem by streaming a tar.bz2 template
148163// and injecting extra files, without extracting to disk.
149164//
150165// Returns the size of the created filesystem image in bytes.
151- func createSquashFsFromTar (buildEnv env.ExecEnv , outputFn , templateFn string , extraFiles []ExtraFile ) (int64 , error ) {
166+ func createSquashFsFromTar (buildEnv env.ExecEnv , outputFn , templateFn string , extraFiles []extraFile ) (int64 , error ) {
152167 const (
153168 sqfstarBin = "sqfstar"
154169 fakerootBin = "fakeroot"
@@ -216,8 +231,18 @@ func createSquashFsFromTar(buildEnv env.ExecEnv, outputFn, templateFn string, ex
216231 tarHasher := sha256 .New ()
217232 tarWriter := tar .NewWriter (io .MultiWriter (stdinPipe , tarHasher ))
218233
234+ // Ensure cleanup on error. On success, we close things explicitly in order.
235+ var cmdErr error
236+ defer func () {
237+ if cmdErr != nil {
238+ tarWriter .Close ()
239+ stdinPipe .Close ()
240+ cmd .Wait () //nolint:errcheck
241+ }
242+ }()
243+
219244 // Build a map of extra files by their tar path for quick lookup.
220- extraFileMap := make (map [string ]ExtraFile )
245+ extraFileMap := make (map [string ]extraFile )
221246 for _ , ef := range extraFiles {
222247 extraFileMap [ef .TarPath ] = ef
223248 }
@@ -229,101 +254,56 @@ func createSquashFsFromTar(buildEnv env.ExecEnv, outputFn, templateFn string, ex
229254 break
230255 }
231256 if err != nil {
232- tarWriter .Close ()
233- stdinPipe .Close ()
234- cmd .Wait () //nolint:errcheck
235- return 0 , fmt .Errorf ("error reading template archive: %w" , err )
236- }
237-
238- // Skip PAX/GNU special headers - these are metadata entries that tar.Reader
239- // normally handles internally, but if we see them we should not emit them.
240- // They have bodies that must be drained.
241- switch header .Typeflag {
242- case tar .TypeXHeader , tar .TypeXGlobalHeader , tar .TypeGNULongName , tar .TypeGNULongLink :
243- //nolint:gosec // G110: This is trusted input from our own template archives.
244- if _ , err := io .Copy (io .Discard , tarReader ); err != nil {
245- tarWriter .Close ()
246- stdinPipe .Close ()
247- cmd .Wait () //nolint:errcheck
248- return 0 , fmt .Errorf ("failed to skip special header content: %w" , err )
249- }
250- continue
257+ cmdErr = fmt .Errorf ("error reading template archive: %w" , err )
258+ return 0 , cmdErr
251259 }
252260
253261 // Normalize the path (remove leading ./).
254262 cleanPath := strings .TrimPrefix (header .Name , "./" )
255263
256264 // Skip if this path will be replaced by an extra file.
257265 if _ , willReplace := extraFileMap [cleanPath ]; willReplace {
258- // Skip this entry, we'll add the replacement later.
259- if header .Typeflag == tar .TypeReg || header .Typeflag == tar .TypeRegA { //nolint:staticcheck // TypeRegA for backward compat
260- // Drain the content.
261- //nolint:gosec // G110: This is trusted input from our own template archives.
262- if _ , err := io .Copy (io .Discard , tarReader ); err != nil {
263- tarWriter .Close ()
264- stdinPipe .Close ()
265- cmd .Wait () //nolint:errcheck
266- return 0 , fmt .Errorf ("failed to skip replaced file content: %w" , err )
267- }
268- }
269266 continue
270267 }
271268
272269 // Normalize header for reproducibility.
273- // Use GNU format because sqfstar < 4.6 has a bug with PAX headers
274- // that incorrectly strips pathname components in linkpath for symlinks.
275- header .Format = tar .FormatGNU
276- header .Uid = 0
277- header .Gid = 0
278- header .Uname = ""
279- header .Gname = ""
280- header .ModTime = time .Unix (0 , 0 )
281- header .AccessTime = time.Time {} // Zero value = not set
282- header .ChangeTime = time.Time {} // Zero value = not set
283- header .PAXRecords = nil
270+ normalizeHeader (header )
284271
285272 if err := tarWriter .WriteHeader (header ); err != nil {
286- tarWriter .Close ()
287- stdinPipe .Close ()
288- cmd .Wait () //nolint:errcheck
289- return 0 , fmt .Errorf ("failed to write tar header: %w" , err )
273+ cmdErr = fmt .Errorf ("failed to write tar header: %w" , err )
274+ return 0 , cmdErr
290275 }
291276
292277 // Copy body for entry types that have content (regular files).
293278 if header .Typeflag == tar .TypeReg || header .Typeflag == tar .TypeRegA { //nolint:staticcheck // TypeRegA for backward compat
294279 //nolint:gosec // G110: This is trusted input from our own template archives.
295280 if _ , err := io .Copy (tarWriter , tarReader ); err != nil {
296- tarWriter .Close ()
297- stdinPipe .Close ()
298- cmd .Wait () //nolint:errcheck
299- return 0 , fmt .Errorf ("failed to copy file content: %w" , err )
281+ cmdErr = fmt .Errorf ("failed to copy file content: %w" , err )
282+ return 0 , cmdErr
300283 }
301284 }
302285 }
303286
304287 // Add extra files.
305288 for _ , ef := range extraFiles {
306289 if err := addFileToTar (tarWriter , ef .HostPath , ef .TarPath , ef .Mode ); err != nil {
307- tarWriter .Close ()
308- stdinPipe .Close ()
309- cmd .Wait () //nolint:errcheck
310- return 0 , fmt .Errorf ("failed to add extra file %s: %w" , ef .TarPath , err )
290+ cmdErr = fmt .Errorf ("failed to add extra file %s: %w" , ef .TarPath , err )
291+ return 0 , cmdErr
311292 }
312293 }
313294
314295 // Close tar writer and stdin pipe.
315296 if err := tarWriter .Close (); err != nil {
316- stdinPipe .Close ()
317- cmd .Wait () //nolint:errcheck
318- return 0 , fmt .Errorf ("failed to close tar writer: %w" , err )
297+ cmdErr = fmt .Errorf ("failed to close tar writer: %w" , err )
298+ return 0 , cmdErr
319299 }
320300
321301 // Print tar hash for verification.
322302 fmt .Printf ("TAR archive SHA256: %s\n " , hex .EncodeToString (tarHasher .Sum (nil )))
323303
324304 if err := stdinPipe .Close (); err != nil {
325- cmd . Wait () //nolint:errcheck
326- return 0 , fmt . Errorf ( "failed to close stdin pipe: %w" , err )
305+ cmdErr = fmt . Errorf ( "failed to close stdin pipe: %w" , err )
306+ return 0 , cmdErr
327307 }
328308
329309 // Wait for sqfstar to finish.
@@ -353,15 +333,12 @@ func addFileToTar(tw *tar.Writer, hostPath, tarPath string, mode os.FileMode) er
353333 }
354334
355335 header := & tar.Header {
356- Format : tar .FormatGNU ,
357336 Name : "./" + tarPath ,
358337 Mode : int64 (mode ),
359338 Size : fi .Size (),
360- Uid : 0 ,
361- Gid : 0 ,
362- ModTime : time .Unix (0 , 0 ),
363339 Typeflag : tar .TypeReg ,
364340 }
341+ normalizeHeader (header )
365342
366343 if err := tw .WriteHeader (header ); err != nil {
367344 return fmt .Errorf ("failed to write header: %w" , err )
0 commit comments