Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ jobs:
- name: Run GoReleaser build
uses: goreleaser/goreleaser-action@v6
with:
args: build
args: build --snapshot --clean
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,8 @@ hold "ffmpeg"

# Use pins to control package source selection
pin "*" 600, release: "l=NVIDIA CUDA"

# Clear apt caches to reduce disk usage (useful for Docker images)
clear-caches
```

16 changes: 16 additions & 0 deletions aptfile/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ type HoldDirective struct {
PackageName string
}

type ClearCachesDirective struct {
}

var (
ErrNoDirective = errors.New("no directive found")
ErrParsing = errors.New("error parsing aptfile")
Expand Down Expand Up @@ -158,6 +161,8 @@ func ParseLine(lineNum int, line string) (any, error) {
return parsePinDirective(cmd, args, opts)
case "hold":
return parseHoldDirective(cmd, args, opts)
case "clear-caches":
return parseClearCachesDirective(cmd, args, opts)
default:
return nil, fmt.Errorf(`unexpected directive "%s"`, cmd)
}
Expand Down Expand Up @@ -296,3 +301,14 @@ func parseHoldDirective(_ string, args []string, opts map[string]string) (HoldDi
PackageName: args[0],
}, nil
}

// clear-caches directive is formatted like, `clear-caches`
func parseClearCachesDirective(_ string, args []string, opts map[string]string) (ClearCachesDirective, error) {
if len(args) > 0 {
return ClearCachesDirective{}, fmt.Errorf("expected no arguments, got %v", args)
}
if len(opts) > 0 {
return ClearCachesDirective{}, fmt.Errorf("unexpected options %v", opts)
}
return ClearCachesDirective{}, nil
}
5 changes: 5 additions & 0 deletions aptfile/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ func TestParseLine(t *testing.T) {
line: "hold curl",
expected: HoldDirective{PackageName: "curl"},
},
{
name: "clear-caches directive",
line: "clear-caches",
expected: ClearCachesDirective{},
},
{
name: "invalid syntax",
line: "package foo: bar",
Expand Down
43 changes: 43 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func processAptfile(path string, dryRun bool) {
}

pkgs := make([]aptfile.PackageDirective, 0)
clearCachesDirectives := make([]aptfile.ClearCachesDirective, 0)
Comment on lines 86 to +87
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

clearCachesDirectives is only used as a presence check (len(...) > 0) and the directive struct is empty, so storing every occurrence is unnecessary. Consider replacing this slice with a simple boolean (e.g., clearCachesRequested) to better express intent and avoid redundant allocations/append calls.

Copilot uses AI. Check for mistakes.

// First pass, skip package installation (except for .deb files,
// which can be necessary for setting up repos or keyrings, etc.)
Expand Down Expand Up @@ -117,6 +118,9 @@ func processAptfile(path string, dryRun bool) {
if err := addHold(dir, dryRun); err != nil {
log.Fatalf("Failed to add hold: %v", err)
}
case aptfile.ClearCachesDirective:
// Defer clear-caches to run after packages are installed
clearCachesDirectives = append(clearCachesDirectives, dir)
default:
log.Fatalf("Unknown directive: %v", d)
}
Expand All @@ -126,6 +130,13 @@ func processAptfile(path string, dryRun bool) {
if err != nil {
log.Fatalf("Failed to install packages: %v", err)
}

// Execute clear-caches once if any directives exist
if len(clearCachesDirectives) > 0 {
if err := clearCaches(dryRun); err != nil {
log.Fatalf("Failed to clear caches: %v", err)
}
}
Comment on lines +134 to +139
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

New clear-caches behavior (deferral until after installPackages and idempotent execution) isn’t covered by tests. Since this file already has unit tests, add a test that runs processAptfile(..., dryRun=true) (or calls clearCaches(true)) while capturing stdout/stderr to assert the directive is executed exactly once and only after the install step.

Copilot uses AI. Check for mistakes.
}

func installPackages(pkgs []aptfile.PackageDirective, dryRun bool) error {
Expand Down Expand Up @@ -401,3 +412,35 @@ func addHold(hold aptfile.HoldDirective, dryRun bool) error {
return fixCmd.Run()
}
}

func clearCaches(dryRun bool) error {
if dryRun {
fmt.Println("[dry-run] Would run `apt-get clean`")
fmt.Println("[dry-run] Would remove /var/lib/apt/lists/*")
return nil
}

fmt.Println("Clearing apt caches...")

// Run apt-get clean to clear package cache
cleanCmd := exec.Command("apt-get", "clean")
cleanCmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
cleanCmd.Stdout = os.Stdout
cleanCmd.Stderr = os.Stderr
if err := cleanCmd.Run(); err != nil {
return fmt.Errorf("error running apt-get clean: %w", err)
}

// Remove apt lists to reduce size further
// Use sh -c to ensure glob expansion works
fmt.Println("Removing apt package lists...")
rmCmd := exec.Command("sh", "-c", "rm -rf /var/lib/apt/lists/*")
rmCmd.Stdout = os.Stdout
rmCmd.Stderr = os.Stderr
if err := rmCmd.Run(); err != nil {
return fmt.Errorf("error removing apt lists: %w", err)
}

fmt.Println("Cache clearing completed")
return nil
}