Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion packaging/linux/deb/debroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@ func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worke
return llb.Scratch(), errors.Wrap(err, "error generating changelog file")
}

// Generate README.Debian with source provenance documentation
readmeDebian := generateReadmeDebian(spec)
var readmeDebianState llb.State
if readmeDebian != nil {
readmeDebianState = in.
File(llb.Mkdir(filepath.Join(dir), 0o755, llb.WithParents(true))).
File(llb.Mkfile(filepath.Join(dir, "README.Debian"), 0o644, readmeDebian))
} else {
readmeDebianState = in.File(llb.Mkdir(filepath.Join(dir), 0o755, llb.WithParents(true)))
}

if dir == "" {
dir = "debian"
}
Expand All @@ -138,7 +149,7 @@ func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worke
File(llb.Mkdir(filepath.Join(dir, "dalec"), 0o755), opts...).
File(llb.Mkfile(filepath.Join(dir, "source/include-binaries"), 0o640, append([]byte("dalec"), '\n')), opts...)

states := []llb.State{control, rules, changelog, debian}
states := []llb.State{control, rules, changelog, readmeDebianState, debian}
states = append(states, installers...)

dalecDir := base.
Expand Down Expand Up @@ -678,3 +689,50 @@ func setSymlinkOwnershipPostInst(w *bytes.Buffer, spec *dalec.Spec, target strin
}
}
}

// generateReadmeDebian creates the content for debian/README.Debian that documents
// source provenance, allowing consumers to understand where sources came from.
func generateReadmeDebian(spec *dalec.Spec) []byte {
if len(spec.Sources) == 0 {
return nil
}

buf := bytes.NewBuffer(nil)

fmt.Fprintf(buf, "%s for Debian\n", spec.Name)
fmt.Fprintf(buf, "%s\n\n", strings.Repeat("=", len(spec.Name)+11)) // Create underline

if spec.Description != "" {
fmt.Fprintf(buf, "%s\n\n", spec.Description)
}

fmt.Fprintf(buf, "Source Provenance\n")
fmt.Fprintf(buf, "-----------------\n\n")
fmt.Fprintf(buf, "This package was built from the following sources:\n\n")

// Sort source names for consistent output
sorted := dalec.SortMapKeys(spec.Sources)
for i, name := range sorted {
if i > 0 {
fmt.Fprintf(buf, "\n")
}

fmt.Fprintf(buf, "Source: %s\n", name)

src := spec.Sources[name]
doc := src.Doc(name)
scanner := bufio.NewScanner(doc)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
fmt.Fprintf(buf, " %s\n", line)
}
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(buf, " Error reading source documentation: %v\n", err)
}
}

fmt.Fprintf(buf, "\n")
return buf.Bytes()
}
134 changes: 134 additions & 0 deletions packaging/linux/deb/readme_debian_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package deb

import (
"bufio"
"bytes"
"strings"
"testing"

"github.com/Azure/dalec"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot The import path is now github.com/project-dalec/dalec

"gotest.tools/v3/assert"
)

func TestGenerateReadmeDebian(t *testing.T) {
t.Run("no sources", func(t *testing.T) {
spec := &dalec.Spec{
Name: "test-package",
Description: "Test package description",
}

result := generateReadmeDebian(spec)

// Should return nil when there are no sources
assert.Assert(t, result == nil)
})

t.Run("single inline source", func(t *testing.T) {
spec := &dalec.Spec{
Name: "test-package",
Description: "Test package description",
Sources: map[string]dalec.Source{
"config.txt": {
Inline: &dalec.SourceInline{
File: &dalec.SourceInlineFile{
Contents: "test config content",
},
},
},
},
}

result := generateReadmeDebian(spec)
content := string(result)

assert.Assert(t, strings.Contains(content, "test-package for Debian"))
assert.Assert(t, strings.Contains(content, "Test package description"))
assert.Assert(t, strings.Contains(content, "Source Provenance"))
assert.Assert(t, strings.Contains(content, "Source: config.txt"))
assert.Assert(t, strings.Contains(content, "Generated from an inline source"))
})

t.Run("multiple sources", func(t *testing.T) {
spec := &dalec.Spec{
Name: "multi-source-package",
Description: "Package with multiple sources",
Sources: map[string]dalec.Source{
"config1.txt": {
Inline: &dalec.SourceInline{
File: &dalec.SourceInlineFile{
Contents: "config1 content",
},
},
},
"config2.txt": {
Inline: &dalec.SourceInline{
File: &dalec.SourceInlineFile{
Contents: "config2 content",
},
},
},
},
}

result := generateReadmeDebian(spec)
content := string(result)

assert.Assert(t, strings.Contains(content, "multi-source-package for Debian"))
assert.Assert(t, strings.Contains(content, "Package with multiple sources"))
assert.Assert(t, strings.Contains(content, "Source Provenance"))
assert.Assert(t, strings.Contains(content, "Source: config1.txt"))
assert.Assert(t, strings.Contains(content, "Source: config2.txt"))

// Verify sources are ordered (sorted by name)
lines := strings.Split(content, "\n")
config1Index := -1
config2Index := -1
for i, line := range lines {
if strings.Contains(line, "Source: config1.txt") {
config1Index = i
}
if strings.Contains(line, "Source: config2.txt") {
config2Index = i
}
}
assert.Assert(t, config1Index >= 0 && config2Index >= 0)
assert.Assert(t, config1Index < config2Index, "Sources should be sorted alphabetically")
})

t.Run("source documentation is indented", func(t *testing.T) {
spec := &dalec.Spec{
Name: "test-package",
Sources: map[string]dalec.Source{
"test-file": {
Inline: &dalec.SourceInline{
File: &dalec.SourceInlineFile{
Contents: "test content",
},
},
},
},
}

result := generateReadmeDebian(spec)

// Parse the result to verify indentation
scanner := bufio.NewScanner(bytes.NewReader(result))
foundSourceLine := false
foundIndentedDoc := false

for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "Source: test-file") {
foundSourceLine = true
continue
}
if foundSourceLine && strings.HasPrefix(line, " ") && strings.Contains(line, "Generated from an inline source") {
foundIndentedDoc = true
break
}
}

assert.Assert(t, foundSourceLine, "Should find source line")
assert.Assert(t, foundIndentedDoc, "Should find indented documentation")
})
}
Loading