@@ -21,6 +21,7 @@ import (
2121 "github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
2222 "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
2323 "github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
24+ "github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders/fedorasource"
2425 "github.com/microsoft/azure-linux-dev-tools/internal/utils/dirdiff"
2526 "github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms"
2627 "github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
@@ -84,6 +85,24 @@ func WithSkipLookaside() PreparerOption {
8485 }
8586}
8687
88+ // WithAllowNoHashes returns a [PreparerOption] that allows source file
89+ // references to omit their [projectconfig.SourceFileReference.Hash] value.
90+ // When set, any source file that lacks a hash will have its hash computed
91+ // from the already-downloaded file in the output directory. If
92+ // [projectconfig.SourceFileReference.HashType] is also missing, SHA-512 is
93+ // used as the default. A warning is emitted for each file whose hash is
94+ // auto-computed.
95+ //
96+ // When combined with [WithSkipLookaside], source file downloads are skipped,
97+ // so files needed for hash computation are not available on disk. In that
98+ // case, missing hashes cannot be auto-computed and source preparation
99+ // returns an error.
100+ func WithAllowNoHashes () PreparerOption {
101+ return func (p * sourcePreparerImpl ) {
102+ p .allowNoHashes = true
103+ }
104+ }
105+
87106// Standard implementation of the [SourcePreparer] interface.
88107type sourcePreparerImpl struct {
89108 sourceManager sourceproviders.SourceManager
@@ -98,9 +117,14 @@ type sourcePreparerImpl struct {
98117 // skipLookaside, when true, skips all lookaside cache downloads during
99118 // source preparation. Git-tracked files are still fetched.
100119 skipLookaside bool
120+
101121 // defaultAuthorEmail is the email address used for synthetic changelog
102122 // entries and commits when no author email is available from git history.
103123 defaultAuthorEmail string
124+
125+ // allowNoHashes, when true, allows source file references without hash
126+ // values. Missing hashes are computed from the downloaded files.
127+ allowNoHashes bool
104128}
105129
106130// NewPreparer creates a new [SourcePreparer] instance. All positional arguments
@@ -184,12 +208,21 @@ func (p *sourcePreparerImpl) PrepareSources(
184208 return err
185209 }
186210
187- // Record the changes as synthetic git history when dist-git creation is enabled.
188- if p .withGitRepo {
189- if err := p .trySyntheticHistory (component , outputDir ); err != nil {
190- return fmt .Errorf ("failed to generate synthetic history for component %#q:\n %w" ,
191- component .GetName (), err )
192- }
211+ if err := p .updateSourcesFile (component , outputDir ); err != nil {
212+ return fmt .Errorf ("failed to update 'sources' file for component %#q:\n %w" ,
213+ component .GetName (), err )
214+ }
215+ } else {
216+ slog .Warn ("Sources prepared without applying overlays;" +
217+ " 'sources' file will not include entries from the 'source-files' configuration" ,
218+ "component" , component .GetName ())
219+ }
220+
221+ // Record the changes as synthetic git history when dist-git creation is enabled.
222+ if p .withGitRepo {
223+ if err := p .trySyntheticHistory (component , outputDir ); err != nil {
224+ return fmt .Errorf ("failed to generate synthetic history for component %#q:\n %w" ,
225+ component .GetName (), err )
193226 }
194227 }
195228
@@ -431,6 +464,195 @@ func (p *sourcePreparerImpl) DiffSources(
431464 return result , nil
432465}
433466
467+ // updateSourcesFile appends entries to the 'sources' file for any extra source files
468+ // defined in the component's [projectconfig.SourceFileReference] configuration. Each source file reference
469+ // must have both [projectconfig.SourceFileReference.Hash] and [projectconfig.SourceFileReference.HashType]
470+ // specified unless [sourcePreparerImpl.allowNoHashes] is set, in which case
471+ // missing hashes are computed from the downloaded files in outputDir.
472+ // Conflicts between existing entries in the 'sources' file and sources introduced through
473+ // the [projectconfig.SourceFileReference] configuration are also treated as errors.
474+ func (p * sourcePreparerImpl ) updateSourcesFile (component components.Component , outputDir string ) error {
475+ sourceFiles := component .GetConfig ().SourceFiles
476+ if len (sourceFiles ) == 0 {
477+ return nil
478+ }
479+
480+ sourcesFilePath := filepath .Join (outputDir , fedorasource .SourcesFileName )
481+
482+ existingContent , err := p .readSourcesFileIfExists (sourcesFilePath )
483+ if err != nil {
484+ return err
485+ }
486+
487+ existingEntries , err := fedorasource .ReadSourcesFileEntries (existingContent )
488+ if err != nil {
489+ return fmt .Errorf ("failed to parse existing sources file %#q:\n %w" , sourcesFilePath , err )
490+ }
491+
492+ existingFilenames := make (map [string ]bool , len (existingEntries ))
493+ for _ , entry := range existingEntries {
494+ if existingFilenames [entry .Filename ] {
495+ return fmt .Errorf (
496+ "failed to process existing 'sources' file %#q: duplicate filename %#q" ,
497+ sourcesFilePath , entry .Filename )
498+ }
499+
500+ existingFilenames [entry .Filename ] = true
501+ }
502+
503+ newEntries , err := p .buildSourceEntries (sourceFiles , existingFilenames , component .GetName (), outputDir )
504+ if err != nil {
505+ return err
506+ }
507+
508+ // Ensure existing content ends with a newline before appending.
509+ if existingContent != "" && ! strings .HasSuffix (existingContent , "\n " ) {
510+ existingContent += "\n "
511+ }
512+
513+ newContent := existingContent + strings .Join (newEntries , "\n " ) + "\n "
514+
515+ if err := fileutils .WriteFile (
516+ p .fs ,
517+ sourcesFilePath ,
518+ []byte (newContent ),
519+ fileperms .PublicFile ,
520+ ); err != nil {
521+ return fmt .Errorf ("failed to write sources file %#q:\n %w" , sourcesFilePath , err )
522+ }
523+
524+ slog .Info ("Updated sources file with extra source file entries" ,
525+ "count" , len (newEntries ),
526+ "path" , sourcesFilePath )
527+
528+ return nil
529+ }
530+
531+ // readSourcesFileIfExists reads the sources file content if it exists, returning empty string if not.
532+ func (p * sourcePreparerImpl ) readSourcesFileIfExists (sourcesFilePath string ) (string , error ) {
533+ exists , err := fileutils .Exists (p .fs , sourcesFilePath )
534+ if err != nil {
535+ return "" , fmt .Errorf ("failed to check if sources file %#q exists:\n %w" , sourcesFilePath , err )
536+ }
537+
538+ if ! exists {
539+ return "" , nil
540+ }
541+
542+ data , err := fileutils .ReadFile (p .fs , sourcesFilePath )
543+ if err != nil {
544+ return "" , fmt .Errorf ("failed to read sources file %#q:\n %w" , sourcesFilePath , err )
545+ }
546+
547+ return string (data ), nil
548+ }
549+
550+ // buildSourceEntries validates [projectconfig.SourceFileReference] and collects formatted entries.
551+ // When [sourcePreparerImpl.allowNoHashes] is true, missing hashes are computed from the downloaded files
552+ // in [outputDir]. When [sourcePreparerImpl.skipLookaside] is also true, hash computation is skipped
553+ // because the source files were not downloaded.
554+ func (p * sourcePreparerImpl ) buildSourceEntries (
555+ sourceFiles []projectconfig.SourceFileReference ,
556+ existingFilenames map [string ]bool ,
557+ componentName string ,
558+ outputDir string ,
559+ ) ([]string , error ) {
560+ newEntries := make ([]string , 0 , len (sourceFiles ))
561+
562+ for _ , ref := range sourceFiles {
563+ if err := fileutils .ValidateFilename (ref .Filename ); err != nil {
564+ return nil , fmt .Errorf ("invalid filename %#q in 'source-files' configuration:\n %w" , ref .Filename , err )
565+ }
566+
567+ if existingFilenames [ref .Filename ] {
568+ return nil , fmt .Errorf (
569+ "source file %#q in 'source-files' configuration conflicts with an existing entry in the 'sources' file; " +
570+ "to overwrite the existing entry, ensure it's removed first; " +
571+ "if this is unintentional, use a different filename" ,
572+ ref .Filename )
573+ }
574+
575+ hash , hashType , err := p .resolveSourceHash (ref , componentName , outputDir )
576+ if err != nil {
577+ return nil , err
578+ }
579+
580+ entry := fedorasource .FormatSourcesEntry (ref .Filename , hashType , hash )
581+ newEntries = append (newEntries , entry )
582+
583+ slog .Debug ("New 'sources' file entry" ,
584+ "filename" , ref .Filename ,
585+ "hashType" , hashType ,
586+ "hash" , hash )
587+ }
588+
589+ return newEntries , nil
590+ }
591+
592+ // resolveSourceHash returns the hash and hash type for a source file reference.
593+ // If the reference already has both values, they are returned as-is. When [ref.Hash]
594+ // is missing and '--allow-no-hashes' is set, the hash is computed from the
595+ // downloaded file on disk. Returns an error when the hash cannot be resolved.
596+ func (p * sourcePreparerImpl ) resolveSourceHash (
597+ ref projectconfig.SourceFileReference , componentName string , outputDir string ,
598+ ) (hash string , hashType fileutils.HashType , err error ) {
599+ hash = ref .Hash
600+ hashType = ref .HashType
601+
602+ if hash != "" {
603+ if hashType == "" {
604+ return "" , "" , fmt .Errorf (
605+ "source file %#q has a 'hash' value but no 'hash-type'; " +
606+ "both must be specified in the 'source-files' configuration;" +
607+ "alternatively 'hash' can be omitted and 'prep-sources' run with " +
608+ "'--allow-no-hashes' to compute missing hashes automatically" ,
609+ ref .Filename )
610+ }
611+
612+ return hash , hashType , nil
613+ }
614+
615+ // Hash is empty — resolve it if '--allow-no-hashes' is set.
616+ if ! p .allowNoHashes {
617+ return "" , "" , fmt .Errorf (
618+ "source file %#q is missing required 'hash'; specify the hash in the " +
619+ "'source-files' configuration or use '--allow-no-hashes' to compute it automatically" ,
620+ ref .Filename )
621+ }
622+
623+ if p .skipLookaside {
624+ return "" , "" , fmt .Errorf (
625+ "source file %#q is missing its hash and source file downloads were skipped; " +
626+ "hash cannot be computed without the downloaded file" ,
627+ ref .Filename )
628+ }
629+
630+ if hashType == "" {
631+ hashType = fileutils .HashTypeSHA512
632+
633+ slog .Warn ("No 'hash-type' specified for source file; defaulting to SHA-512" ,
634+ "component" , componentName ,
635+ "filename" , ref .Filename ,
636+ "hashType" , hashType )
637+ }
638+
639+ filePath := filepath .Join (outputDir , ref .Filename )
640+
641+ hash , err = fileutils .ComputeFileHash (p .fs , hashType , filePath )
642+ if err != nil {
643+ return "" , "" , fmt .Errorf ("failed to compute hash for source file %#q:\n %w" ,
644+ ref .Filename , err )
645+ }
646+
647+ slog .Warn ("Auto-computed hash for source file; consider adding 'hash' to the configuration" ,
648+ "component" , componentName ,
649+ "filename" , ref .Filename ,
650+ "hashType" , hashType ,
651+ "hash" , hash )
652+
653+ return hash , hashType , nil
654+ }
655+
434656// writeMacrosFile writes a macros file containing the resolved macros for a component.
435657// This includes with/without flags converted to macro format, and any explicit defines.
436658// If the build configuration produces no macros, no file is written and an empty path is
0 commit comments