Skip to content

Commit 862a871

Browse files
authored
feat(cli): Rework prune and self-update to respect installation style (#91)
1 parent bc65934 commit 862a871

23 files changed

Lines changed: 2516 additions & 341 deletions

cli/cmd/logging.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import (
88
)
99

1010
// activateLogging opens the log file and configures the output printer's log writer.
11-
// If logFilePath is set, it uses that directly. Otherwise, it derives the log path
12-
// from the project cache directory. If both are empty, no log file is opened.
13-
func activateLogging(logFilePath string, projectCachePath string) {
11+
// If logFilePath is set, it uses that directly. Otherwise, it writes a timestamped
12+
// log file into logDir. If both are empty, no log file is opened.
13+
func activateLogging(logFilePath string, logDir string) {
1414
var logPath string
1515
var err error
1616

@@ -19,8 +19,8 @@ func activateLogging(logFilePath string, projectCachePath string) {
1919
if _, err = log.OpenLogFileAt(logPath); err != nil {
2020
out.Fatalf("Failed to open log file: %s", err)
2121
}
22-
} else if projectCachePath != "" {
23-
logPath, err = log.OpenProjectLog(projectCachePath)
22+
} else if logDir != "" {
23+
logPath, err = log.OpenProjectLog(logDir)
2424
if err != nil {
2525
out.Fatalf("Failed to open project log file: %s", err)
2626
}
@@ -32,13 +32,12 @@ func activateLogging(logFilePath string, projectCachePath string) {
3232
}
3333
}
3434

35-
// activateLoggingForProject resolves the project cache path from projectPath,
36-
// then activates logging. Used by compile and project commands that share the
37-
// same "resolve cache path → activate logging" pattern.
35+
// activateLoggingForProject resolves the project log directory from projectPath,
36+
// then activates logging. Logs are written to ~/.opentaint/logs/<slug-hash>/.
3837
func activateLoggingForProject(logFilePath string, projectPath string) {
39-
cachePath, err := utils.GetProjectCachePath(projectPath)
38+
logPath, err := utils.GetProjectLogPath(projectPath)
4039
if err != nil {
41-
output.LogInfof("Failed to resolve project cache path for logging: %v", err)
40+
output.LogInfof("Failed to resolve project log path: %v", err)
4241
}
43-
activateLogging(logFilePath, cachePath)
42+
activateLogging(logFilePath, logPath)
4443
}

cli/cmd/prune.go

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,59 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
56

67
"github.com/seqra/opentaint/internal/output"
78
"github.com/seqra/opentaint/internal/utils"
89
"github.com/spf13/cobra"
910
)
1011

1112
var (
12-
pruneDryRun bool
13-
pruneYes bool
14-
pruneIncLogs bool
13+
pruneDryRun bool
14+
pruneYes bool
15+
pruneAll bool
16+
pruneArtifacts bool
17+
pruneRules bool
18+
pruneJDK bool
19+
pruneModels bool
20+
pruneLogs bool
21+
pruneInstall bool
1522
)
1623

24+
// resolveCategories maps CLI flags to a PruneCategory bitmask.
25+
// Returns an error if --all is combined with specific flags.
26+
func resolveCategories() (utils.PruneCategory, error) {
27+
specific := pruneArtifacts || pruneRules || pruneJDK || pruneModels || pruneLogs || pruneInstall
28+
if pruneAll && specific {
29+
return 0, fmt.Errorf("--all cannot be combined with specific category flags (--artifacts, --rules, --jdk, --models, --logs, --install)")
30+
}
31+
if pruneAll {
32+
return utils.PruneCategoriesAll, nil
33+
}
34+
if !specific {
35+
return utils.PruneCategoriesDefault, nil
36+
}
37+
38+
flagMap := []struct {
39+
flag *bool
40+
cat utils.PruneCategory
41+
}{
42+
{&pruneArtifacts, utils.PruneCategoryArtifacts},
43+
{&pruneRules, utils.PruneCategoryRules},
44+
{&pruneJDK, utils.PruneCategoryJDK},
45+
{&pruneModels, utils.PruneCategoryModels},
46+
{&pruneLogs, utils.PruneCategoryLogs},
47+
{&pruneInstall, utils.PruneCategoryInstall},
48+
}
49+
var cats utils.PruneCategory
50+
for _, f := range flagMap {
51+
if *f.flag {
52+
cats |= f.cat
53+
}
54+
}
55+
return cats, nil
56+
}
57+
1758
var pruneCmd = &cobra.Command{
1859
Use: "prune",
1960
Short: "Remove stale downloaded artifacts from ~/.opentaint",
@@ -22,16 +63,59 @@ var pruneCmd = &cobra.Command{
2263
Identifies artifacts that are no longer needed:
2364
- Old versions of analyzer JARs, autobuilder JARs, and rules
2465
- Downloaded JDK/JRE versions that don't match the current version
25-
- Redundant downloads when bundled artifacts are available
26-
- Stale install-tier artifacts (~/.opentaint/install/) after a opentaint upgrade
66+
- Cached project models and staging directories
67+
68+
Use category flags to prune selectively:
69+
--artifacts Stale analyzer and autobuilder JARs
70+
--rules Stale rules directories
71+
--jdk Old JDK/JRE versions
72+
--models Cached project models and staging directories
73+
--logs Project log files
74+
--install Install-tier lib and JRE artifacts (requires re-download)
2775
28-
By default, log files are kept. Use --include-logs to also prune them from project cache directories.`,
76+
Without category flags, prunes: artifacts + rules + jdk + models.
77+
With --all: prunes everything including logs and install-tier.`,
2978
Run: func(cmd *cobra.Command, args []string) {
30-
result, err := utils.ScanForStaleArtifacts(pruneIncLogs)
79+
categories, err := resolveCategories()
80+
if err != nil {
81+
out.FatalErr(err)
82+
}
83+
84+
// Acquire global prune lock
85+
pruneLockPath, err := utils.PruneLockPath()
86+
if err != nil {
87+
out.Fatalf("Failed to resolve prune lock path: %s", err)
88+
}
89+
pruneLock, err := utils.TryLock(pruneLockPath, utils.LockMeta{
90+
PID: os.Getpid(),
91+
Command: "prune",
92+
})
93+
if err == utils.ErrLocked {
94+
out.Fatal("Another prune is already running")
95+
}
96+
if err != nil {
97+
out.Fatalf("Failed to acquire prune lock: %s", err)
98+
}
99+
defer pruneLock.Unlock()
100+
101+
result, err := utils.ScanForStaleArtifacts(categories)
31102
if err != nil {
32103
out.Fatalf("Failed to scan for stale artifacts: %s", err)
33104
}
34105

106+
// Display skipped projects
107+
if len(result.Skipped) > 0 {
108+
sb := out.Section("Skipped (compilation in progress)")
109+
for _, s := range result.Skipped {
110+
if s.Meta.PID != 0 {
111+
sb.Text(fmt.Sprintf("%s (locked by PID %d)", s.Path, s.Meta.PID))
112+
} else {
113+
sb.Text(fmt.Sprintf("%s (locked)", s.Path))
114+
}
115+
}
116+
sb.Render()
117+
}
118+
35119
if result.TotalCount == 0 {
36120
out.Print("No stale artifacts found. Nothing to prune.")
37121
return
@@ -70,5 +154,11 @@ func init() {
70154

71155
pruneCmd.Flags().BoolVar(&pruneDryRun, "dry-run", false, "Show what would be deleted without deleting")
72156
pruneCmd.Flags().BoolVar(&pruneYes, "yes", false, "Skip interactive confirmation")
73-
pruneCmd.Flags().BoolVar(&pruneIncLogs, "include-logs", false, "Also prune log files")
157+
pruneCmd.Flags().BoolVar(&pruneAll, "all", false, "Prune everything including logs and install-tier artifacts")
158+
pruneCmd.Flags().BoolVar(&pruneArtifacts, "artifacts", false, "Prune stale analyzer and autobuilder JARs")
159+
pruneCmd.Flags().BoolVar(&pruneRules, "rules", false, "Prune stale rules directories")
160+
pruneCmd.Flags().BoolVar(&pruneJDK, "jdk", false, "Prune old JDK/JRE versions")
161+
pruneCmd.Flags().BoolVar(&pruneModels, "models", false, "Prune cached project models and staging directories")
162+
pruneCmd.Flags().BoolVar(&pruneLogs, "logs", false, "Prune project log files")
163+
pruneCmd.Flags().BoolVar(&pruneInstall, "install", false, "Prune install-tier lib and JRE artifacts (requires re-download on next run)")
74164
}

cli/cmd/pull.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func downloadJava(installNextToBinary, installCurrent bool) (*tree.Tree, error)
125125
return node, fmt.Errorf("unsupported Java version: %d (supported range: 8-25)", javaVersion)
126126
}
127127

128-
opentaintHome, err := utils.GetOpentaintHome()
128+
opentaintHome, err := utils.GetOpenTaintHome()
129129
if err != nil {
130130
return node, err
131131
}

cli/cmd/root.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ var rootCmd = &cobra.Command{
4848
out.Configure(globals.Config.Log.Color, globals.Config.Quiet)
4949
out.SetVerbosity(globals.Config.Log.Verbosity)
5050

51+
// Reconcile install-tier version marker if needed (lightweight: a few Stat calls).
52+
utils.ReconcileInstallMarker()
53+
5154
// Start async update check (non-blocking, at most once per day)
5255
if !globals.Config.Quiet {
5356
go checkForUpdateAsync()
@@ -184,7 +187,7 @@ func checkForUpdateAsync() {
184187
return
185188
}
186189

187-
opentaintHome, err := utils.GetOpentaintHome()
190+
opentaintHome, err := utils.GetOpenTaintHome()
188191
if err != nil {
189192
return
190193
}

cli/cmd/scan.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type scanConfig struct {
6969
projectCachePath string // cache dir for this project (empty for explicit model / dry-run)
7070
stagingDir string // staging dir path (empty when not compiling or dry-run)
7171
needsCompilation bool // true when compilation is needed before scanning
72+
compileLock *utils.FileLock
7273
}
7374

7475
// scanCmd represents the scan command
@@ -159,14 +160,15 @@ func scan(cmd *cobra.Command) {
159160
}
160161

161162
cfg := resolveScanConfig(absUserProjectRoot)
163+
defer func() {
164+
if cfg.compileLock != nil {
165+
cfg.compileLock.Unlock()
166+
}
167+
}()
162168

163169
// Activate logging
164170
if !DryRunScan {
165-
if cfg.projectCachePath != "" {
166-
activateLogging(ScanLogFile, cfg.projectCachePath)
167-
} else {
168-
activateLoggingForProject(ScanLogFile, absUserProjectRoot)
169-
}
171+
activateLoggingForProject(ScanLogFile, absUserProjectRoot)
170172
}
171173

172174
absProjectModelPath := cfg.absProjectModel
@@ -422,11 +424,18 @@ func resolveScanConfig(absUserProjectRoot string) scanConfig {
422424
}
423425
}
424426

425-
if utils.HasStagingDir(projectCachePath) {
427+
compileLock, lockErr := utils.TryLock(
428+
utils.CompileLockPath(projectCachePath),
429+
utils.LockMeta{PID: os.Getpid(), Command: "compile", Project: absUserProjectRoot},
430+
)
431+
if lockErr == utils.ErrLocked {
426432
out.Error("Compilation already in progress for this project")
427433
suggest("To scan an existing model instead", utils.NewScanCommand("").WithProjectModel("<model-path>").Build())
428434
os.Exit(1)
429435
}
436+
if lockErr != nil {
437+
out.Fatalf("Failed to acquire compile lock: %s", lockErr)
438+
}
430439

431440
stagingDir, serr := utils.CreateStagingDir(projectCachePath)
432441
if serr != nil {
@@ -439,6 +448,7 @@ func resolveScanConfig(absUserProjectRoot string) scanConfig {
439448
projectCachePath: projectCachePath,
440449
stagingDir: stagingDir,
441450
needsCompilation: true,
451+
compileLock: compileLock,
442452
}
443453
}
444454

cli/cmd/update.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"strings"
88

99
"github.com/seqra/opentaint/internal/globals"
10-
"github.com/seqra/opentaint/internal/output"
1110
"github.com/seqra/opentaint/internal/utils"
1211
"github.com/seqra/opentaint/internal/version"
1312
"github.com/spf13/cobra"
@@ -129,21 +128,7 @@ Only upgrades are supported — downgrading to an older version is refused.`,
129128
}
130129

131130
out.Successf("Successfully updated to v%s", targetVersion)
132-
133-
// Auto-prune after successful update
134-
out.Print("Pruning stale artifacts...")
135-
pruneResult, err := utils.ScanForStaleArtifacts(false)
136-
if err != nil {
137-
out.Warnf("Failed to scan for stale artifacts: %s", err)
138-
return
139-
}
140-
if pruneResult.TotalCount > 0 {
141-
if err := utils.DeleteArtifacts(pruneResult.Stale); err != nil {
142-
out.Warnf("Failed to prune stale artifacts: %s", err)
143-
} else {
144-
out.Successf("Pruned %d items, freed %s", pruneResult.TotalCount, output.FormatSize(pruneResult.TotalSize))
145-
}
146-
}
131+
suggest("To clean up old artifacts run", "opentaint prune")
147132
},
148133
}
149134

cli/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ require (
2929
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
3030
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
3131
github.com/fsnotify/fsnotify v1.4.7 // indirect
32+
github.com/gofrs/flock v0.13.0 // indirect
3233
github.com/google/go-querystring v1.1.0 // indirect
3334
github.com/hashicorp/hcl v1.0.0 // indirect
3435
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -48,7 +49,7 @@ require (
4849
github.com/spf13/cast v1.7.1 // indirect
4950
github.com/spf13/jwalterweatherman v1.1.0 // indirect
5051
github.com/spf13/pflag v1.0.6 // indirect
51-
github.com/stretchr/testify v1.10.0 // indirect
52+
github.com/stretchr/testify v1.11.1 // indirect
5253
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
5354
golang.org/x/sync v0.18.0 // indirect
5455
golang.org/x/sys v0.41.0 // indirect

cli/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
4242
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
4343
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
4444
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
45+
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
46+
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
4547
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4648
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
4749
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -106,6 +108,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
106108
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
107109
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
108110
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
111+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
112+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
109113
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
110114
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
111115
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=

cli/internal/utils/java/runner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ func (j *javaRunner) ensureSpecificVersion(version int) (string, error) {
372372
// Unset Java environment variables for clean environment when using specific version
373373
unsetJavaEnvironmentVariables()
374374

375-
opentaintHome, err := utils.GetOpentaintHome()
375+
opentaintHome, err := utils.GetOpenTaintHome()
376376
if err != nil {
377377
return "", err
378378
}

0 commit comments

Comments
 (0)