Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
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
}
Loading