diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d3b238..1a52a9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,4 +38,4 @@ jobs: - name: Run GoReleaser build uses: goreleaser/goreleaser-action@v6 with: - args: build + args: build --snapshot --clean diff --git a/README.md b/README.md index 3327081..7f4b7aa 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/aptfile/main.go b/aptfile/main.go index 73efd6e..46afb3f 100644 --- a/aptfile/main.go +++ b/aptfile/main.go @@ -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") @@ -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) } @@ -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 +} diff --git a/aptfile/main_test.go b/aptfile/main_test.go index ec70c30..346b108 100644 --- a/aptfile/main_test.go +++ b/aptfile/main_test.go @@ -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", diff --git a/main.go b/main.go index fa840df..394781c 100644 --- a/main.go +++ b/main.go @@ -84,6 +84,7 @@ func processAptfile(path string, dryRun bool) { } pkgs := make([]aptfile.PackageDirective, 0) + clearCachesDirectives := make([]aptfile.ClearCachesDirective, 0) // First pass, skip package installation (except for .deb files, // which can be necessary for setting up repos or keyrings, etc.) @@ -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) } @@ -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) + } + } } func installPackages(pkgs []aptfile.PackageDirective, dryRun bool) error { @@ -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 +}