Skip to content

Commit b6a9106

Browse files
authored
fix: make sources-files update sources (#69)
Modifies how `source-files` config affects the output `sources` file: - New files will be appended to the `sources` file. - The `source-files` entries must have their hash and hash type provided unless the user passes the `--allow-no-hashes` flag, which will then retrieve the sources and calculate the missing hashes. - If a file name already exists in the `sources` file, it's considered an error. The user is told to either remove the original entry through an overlay or to re-name the file from `source-files`. Also adds config validation for the `source-files` section of the TOML and updates the code to work only with the hash types defined in the `HashType*` constants.
1 parent 77d2464 commit b6a9106

File tree

19 files changed

+1079
-69
lines changed

19 files changed

+1079
-69
lines changed

.github/instructions/go.instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ description: "Instructions for working on the azldev Go codebase. IMPORTANT: Alw
4141
- `return fmt.Errorf("failed to open %#q:\n%w", filename, err)`
4242
- `return fmt.Errorf("failed to run command 'go %s':\n%w", strings.Join(args, " "), err)`
4343
- Comments referring to types should encapsulate the type name in square brackets. Example: `// [packagename.MyType] is a custom type`
44+
- Config field names and CLI flags in comments and error messages:
45+
- In code comments, use square brackets for field names: `[module.StructName.FieldName]`
46+
- In code comments, use single quotes for flag names: `'--flag-name'`
47+
- In log messages and error strings, use single quotes: `'field-name'`, `'--flag-name'`
4448
- Use structured logging with slog
4549
- Ensure code passes golangci-lint checks
4650
- Use `github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms` instead of re-defining file permission constants

docs/user/reference/cli/azldev_component_prepare-sources.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/user/reference/config/components.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,8 @@ The `[[components.<name>.source-files]]` array defines additional source files t
251251
| Field | TOML Key | Type | Required | Description |
252252
|-------|----------|------|----------|-------------|
253253
| Filename | `filename` | string | **Yes** | Name of the file as it will appear in the sources directory |
254-
| Hash | `hash` | string | No | Expected hash of the downloaded file for integrity verification |
255-
| Hash type | `hash-type` | string | No | Hash algorithm used (e.g., `"SHA512"`, `"SHA256"`) |
254+
| Hash | `hash` | string | Conditional | Expected hash of the downloaded file for integrity verification. Required for the `prep-sources` command unless `--allow-no-hashes` is used, in which case the hash is computed automatically from the downloaded file. |
255+
| Hash type | `hash-type` | string | Conditional | Hash algorithm used (examples: `"SHA256"`, `"SHA512"`). Required when `hash` is specified. When omitted alongside `hash` for the `prep-sources` command and `--allow-no-hashes` is used, defaults to `"SHA512"`. |
256256
| Origin | `origin` | [Origin](#origin) | **Yes** | Where to download the file from |
257257

258258
### Origin

internal/app/azldev/cmds/component/preparesources.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import (
1919
type PrepareSourcesOptions struct {
2020
ComponentFilter components.ComponentFilter
2121

22-
OutputDir string
23-
SkipOverlays bool
24-
WithGitRepo bool
25-
Force bool
22+
OutputDir string
23+
SkipOverlays bool
24+
WithGitRepo bool
25+
Force bool
26+
AllowNoHashes bool
2627
}
2728

2829
func prepareOnAppInit(_ *azldev.App, sourceCmd *cobra.Command) {
@@ -70,6 +71,8 @@ Only one component may be selected at a time.`,
7071
cmd.Flags().BoolVar(&options.WithGitRepo, "with-git", false,
7172
"Create a dist-git repository with synthetic commit history (requires a project git repository)")
7273
cmd.Flags().BoolVar(&options.Force, "force", false, "delete and recreate the output directory if it already exists")
74+
cmd.Flags().BoolVar(&options.AllowNoHashes, "allow-no-hashes", false,
75+
"compute missing hashes by downloading source files from their origin")
7376

7477
return cmd
7578
}
@@ -128,6 +131,10 @@ func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) er
128131
preparerOpts = append(preparerOpts, sources.WithGitRepo(env.Config().Project.DefaultAuthorEmail))
129132
}
130133

134+
if options.AllowNoHashes {
135+
preparerOpts = append(preparerOpts, sources.WithAllowNoHashes())
136+
}
137+
131138
preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)
132139
if err != nil {
133140
return fmt.Errorf("failed to create source preparer:\n%w", err)

internal/app/azldev/cmds/component/preparesources_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ func TestNewPrepareSourcesCmd(t *testing.T) {
2323
require.NotNil(t, forceFlag, "--force flag should be registered")
2424
assert.Equal(t, "false", forceFlag.DefValue)
2525
assert.Contains(t, forceFlag.Usage, "delete and recreate the output directory")
26+
27+
allowNoHashesFlag := cmd.Flags().Lookup("allow-no-hashes")
28+
require.NotNil(t, allowNoHashesFlag, "--allow-no-hashes flag should be registered")
29+
assert.Equal(t, "false", allowNoHashesFlag.DefValue)
30+
assert.Contains(t, allowNoHashesFlag.Usage, "compute missing hashes")
2631
}
2732

2833
func TestPrepareSourcesCmd_NoMatch(t *testing.T) {

internal/app/azldev/core/sources/sourceprep.go

Lines changed: 228 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
88107
type 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

Comments
 (0)