Skip to content

Commit bc65934

Browse files
authored
feat(cli): Add scan defaults and compilation model caching (#89)
1 parent 2820140 commit bc65934

52 files changed

Lines changed: 2052 additions & 241 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-cli.yaml

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,94 @@ jobs:
141141
working-directory: cli
142142
run: |
143143
./opentaint compile --quiet ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root --verbosity debug
144-
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif portable-project --verbosity debug
144+
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project --verbosity debug
145145
146-
- name: Run opentaint scan
146+
- name: Run opentaint scan with explicit path and output
147147
working-directory: cli
148148
run: |
149149
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif ../project-root
150150
151+
- name: Clean up cached models before default-output tests
152+
working-directory: cli
153+
run: ./opentaint prune --yes ${{ steps.github-token.outputs.arg }}
154+
155+
- name: Run opentaint scan with default output (CompileAndScan)
156+
working-directory: cli
157+
run: |
158+
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} ../project-root
159+
160+
- name: Verify cached model and default SARIF location
161+
run: |
162+
# Exactly one project cache directory should exist
163+
MODEL_COUNT=$(ls -d ~/.opentaint/cache/*/ 2>/dev/null | wc -l)
164+
if [ "$MODEL_COUNT" -ne 1 ]; then
165+
echo "Expected 1 cached model directory, found $MODEL_COUNT"
166+
ls -la ~/.opentaint/cache/ || true
167+
exit 1
168+
fi
169+
170+
CACHE_DIR=$(ls -d ~/.opentaint/cache/*/)
171+
172+
# project-model directory should exist
173+
if [ ! -d "${CACHE_DIR}project-model" ]; then
174+
echo "Expected project-model directory in $CACHE_DIR"
175+
ls -la "$CACHE_DIR"
176+
exit 1
177+
fi
178+
179+
# Default SARIF should be inside project-model/sources/
180+
SARIF_PATH="${CACHE_DIR}project-model/sources/opentaint.sarif"
181+
if [ ! -f "$SARIF_PATH" ]; then
182+
echo "Expected default SARIF at $SARIF_PATH"
183+
find "$CACHE_DIR" -name "*.sarif" || true
184+
exit 1
185+
fi
186+
187+
echo "Cached model verified at: $CACHE_DIR"
188+
echo "Default SARIF verified at: $SARIF_PATH"
189+
190+
- name: Run opentaint scan with no arguments (defaults to current directory)
191+
working-directory: project-root
192+
run: |
193+
../cli/opentaint scan --quiet ${{ steps.github-token.outputs.arg }}
194+
195+
- name: Run opentaint scan on pre-compiled model without --output
196+
working-directory: cli
197+
run: |
198+
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --project-model portable-project
199+
200+
- name: Verify SARIF placed inside provided project-model
201+
working-directory: cli
202+
run: |
203+
if [ ! -f "portable-project/sources/opentaint.sarif" ]; then
204+
echo "Expected SARIF at portable-project/sources/opentaint.sarif"
205+
find portable-project -name "*.sarif" || true
206+
exit 1
207+
fi
208+
echo "SARIF in explicit project-model verified"
209+
210+
- name: Run opentaint prune --dry-run and verify cached models listed
211+
working-directory: cli
212+
run: |
213+
OUTPUT=$(./opentaint prune --dry-run 2>&1)
214+
if ! echo "$OUTPUT" | grep -q "model"; then
215+
echo "Expected prune --dry-run to list cached model artifacts"
216+
echo "$OUTPUT"
217+
exit 1
218+
fi
219+
echo "Prune dry-run correctly lists cached models"
220+
221+
- name: Run opentaint prune --yes and verify cached models removed
222+
working-directory: cli
223+
run: |
224+
./opentaint prune --yes
225+
if [ -d ~/.opentaint/cache ] && [ "$(ls -A ~/.opentaint/cache 2>/dev/null)" ]; then
226+
echo "Expected cache directory to be empty after prune"
227+
ls -la ~/.opentaint/cache/
228+
exit 1
229+
fi
230+
echo "Prune correctly removed cached models"
231+
151232
run-on-petclinic-windows:
152233
runs-on: windows-latest
153234

@@ -185,7 +266,7 @@ jobs:
185266
working-directory: cli
186267
run: |
187268
./opentaint compile --quiet ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root --verbosity debug
188-
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif portable-project --verbosity debug
269+
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project --verbosity debug
189270
190271
- name: Run opentaint scan
191272
working-directory: cli
@@ -232,7 +313,7 @@ jobs:
232313
- name: Run opentaint scan
233314
working-directory: cli
234315
run: |
235-
./opentaint scan ${{ steps.github-token.outputs.arg }} --output stirling-pdf-report.sarif stirling-pdf-model
316+
./opentaint scan ${{ steps.github-token.outputs.arg }} --output stirling-pdf-report.sarif --project-model stirling-pdf-model
236317
237318
test-install-sh-linux:
238319
runs-on: ubuntu-latest

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ irm https://raw.githubusercontent.com/seqra/opentaint/main/scripts/install/insta
127127

128128
**Scan your project:**
129129
```bash
130-
opentaint scan --output results.sarif /path/to/your/spring/project
130+
opentaint scan
131131
```
132132

133133
**Or use Docker:**

cli/cmd/compile.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ const (
2626
var OutputProjectModelPath string
2727
var ProjectPath string
2828
var DryRunCompile bool
29+
var CompileLogFile string
30+
31+
// currentCompileBuilder returns a builder pre-populated with the user's current compile flags.
32+
// All compile command suggestions should use this as the base to ensure that adding a new
33+
// flag in one place automatically propagates to every suggestion.
34+
func currentCompileBuilder(projectPath string) *utils.OpentaintCommandBuilder {
35+
return utils.NewCompileCommand(projectPath).
36+
WithOutput(OutputProjectModelPath)
37+
}
2938

3039
// compileCmd represents the compile command
3140
var compileCmd = &cobra.Command{
@@ -44,6 +53,15 @@ Arguments:
4453
projectRoot := filepath.Clean(ProjectPath)
4554
absProjectRoot := log.AbsPathOrExit(projectRoot, "project path")
4655

56+
if err := validation.ValidateSourceProjectForCompile(absProjectRoot); err != nil {
57+
out.FatalErr(err)
58+
}
59+
60+
// Activate logging
61+
if !DryRunCompile {
62+
activateLoggingForProject(CompileLogFile, absProjectRoot)
63+
}
64+
4765
outputProjectModelPath := filepath.Clean(OutputProjectModelPath)
4866
absOutputProjectModelPath := log.AbsPathOrExit(outputProjectModelPath, "output")
4967

@@ -97,6 +115,7 @@ func init() {
97115
compileCmd.Flags().StringVarP(&OutputProjectModelPath, "output", "o", "", `Path to the result project model`)
98116
_ = compileCmd.MarkFlagRequired("output")
99117
compileCmd.Flags().BoolVar(&DryRunCompile, "dry-run", false, "Validate inputs and show what would run without compiling")
118+
compileCmd.Flags().StringVar(&CompileLogFile, "log-file", "", "Path to the log file (default: <cache-dir>/logs/<timestamp>.log)")
100119
}
101120

102121
func ensureAutobuilderAvailable() (string, error) {
@@ -131,7 +150,7 @@ func compile(absProjectRoot, absOutputProjectModelPath, autobuilderJarPath strin
131150
validationErr := fmt.Errorf("output validation failed after compile: %w", err)
132151
output.LogInfo(validationErr)
133152
if caller == External {
134-
suggest("If native compilation fails due to missing required Java, set JAVA_HOME according to the project's requirements or try Docker-based compilation:", utils.BuildCompileCommandWithDocker(ProjectPath, OutputProjectModelPath))
153+
suggest("If native compilation fails due to missing required Java, set JAVA_HOME according to the project's requirements or try Docker-based compilation:", utils.BuildCompileCommandWithDocker(currentCompileBuilder(""), ProjectPath, OutputProjectModelPath))
135154
}
136155
return fmt.Errorf("there was a problem during the compile step, check the full logs: %s", globals.LogPath)
137156
}

cli/cmd/logging.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package cmd
2+
3+
import (
4+
"github.com/seqra/opentaint/internal/globals"
5+
"github.com/seqra/opentaint/internal/output"
6+
"github.com/seqra/opentaint/internal/utils"
7+
"github.com/seqra/opentaint/internal/utils/log"
8+
)
9+
10+
// 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) {
14+
var logPath string
15+
var err error
16+
17+
if logFilePath != "" {
18+
logPath = log.AbsPathOrExit(logFilePath, "log file")
19+
if _, err = log.OpenLogFileAt(logPath); err != nil {
20+
out.Fatalf("Failed to open log file: %s", err)
21+
}
22+
} else if projectCachePath != "" {
23+
logPath, err = log.OpenProjectLog(projectCachePath)
24+
if err != nil {
25+
out.Fatalf("Failed to open project log file: %s", err)
26+
}
27+
}
28+
29+
if logPath != "" {
30+
globals.LogPath = logPath
31+
out.SetLogWriter(log.LogWriter())
32+
}
33+
}
34+
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.
38+
func activateLoggingForProject(logFilePath string, projectPath string) {
39+
cachePath, err := utils.GetProjectCachePath(projectPath)
40+
if err != nil {
41+
output.LogInfof("Failed to resolve project cache path for logging: %v", err)
42+
}
43+
activateLogging(logFilePath, cachePath)
44+
}

cli/cmd/project.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,13 @@ func (c *JavaAutobuilderConfig) logProjectSummary(projectYamlPath string, config
206206
}
207207

208208
var (
209-
OutputDir string
210-
SourceRoot string
211-
Dependencies []string
212-
Packages []string
213-
Classpaths []string
214-
DryRunProject bool
209+
OutputDir string
210+
SourceRoot string
211+
Dependencies []string
212+
Packages []string
213+
Classpaths []string
214+
DryRunProject bool
215+
ProjectLogFile string
215216
)
216217

217218
var projectCmd = &cobra.Command{
@@ -235,6 +236,12 @@ Examples:
235236
WithClasspath(Classpaths).
236237
Build()
237238

239+
// Activate logging — derive cache slug from --output path
240+
if !DryRunProject {
241+
outputAbs := log.AbsPathOrExit(filepath.Clean(OutputDir), "output")
242+
activateLoggingForProject(ProjectLogFile, outputAbs)
243+
}
244+
238245
classpathItems := make([]any, len(config.classpaths))
239246
for i, cp := range config.classpaths {
240247
classpathItems[i] = cp
@@ -284,4 +291,5 @@ func init() {
284291
projectCmd.Flags().StringArrayVar(&Classpaths, "classpath", []string{}, "Classpath entries (classes or JAR files)")
285292
_ = projectCmd.MarkFlagRequired("classpath")
286293
projectCmd.Flags().BoolVar(&DryRunProject, "dry-run", false, "Validate inputs and show what would run without generating project model")
294+
projectCmd.Flags().StringVar(&ProjectLogFile, "log-file", "", "Path to the log file (default: <cache-dir>/logs/<timestamp>.log)")
287295
}

cli/cmd/prune.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Identifies artifacts that are no longer needed:
2525
- Redundant downloads when bundled artifacts are available
2626
- Stale install-tier artifacts (~/.opentaint/install/) after a opentaint upgrade
2727
28-
By default, log files are kept. Use --include-logs to also prune them.`,
28+
By default, log files are kept. Use --include-logs to also prune them from project cache directories.`,
2929
Run: func(cmd *cobra.Command, args []string) {
3030
result, err := utils.ScanForStaleArtifacts(pruneIncLogs)
3131
if err != nil {

cli/cmd/root.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,13 @@ var rootCmd = &cobra.Command{
4040
}
4141
globals.Config.Log.Verbosity = verbosity
4242

43-
// Set up logging to both console and file
44-
logFile, logPath, err := log.OpenLogFile()
45-
globals.LogPath = logPath
46-
cobra.CheckErr(err)
47-
48-
if err := log.SetUpLogs(logFile, globals.Config.Log.Verbosity, globals.Config.Log.Color); err != nil {
43+
if err := log.SetUpLogs(globals.Config.Log.Verbosity); err != nil {
4944
return fmt.Errorf("failed to set up logging: %w", err)
5045
}
5146

5247
// Configure the output printer (color mode, quiet mode)
5348
out.Configure(globals.Config.Log.Color, globals.Config.Quiet)
5449
out.SetVerbosity(globals.Config.Log.Verbosity)
55-
out.SetLogWriter(logFile)
5650

5751
// Start async update check (non-blocking, at most once per day)
5852
if !globals.Config.Quiet {
@@ -84,7 +78,7 @@ var rootCmd = &cobra.Command{
8478
func Execute() {
8579
err := rootCmd.Execute()
8680
if err != nil {
87-
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
81+
fmt.Fprintf(os.Stderr, "Error: %s\n", output.Humanize(err))
8882
fmt.Fprintln(os.Stderr, "Run 'opentaint --help' for usage.")
8983
os.Exit(1)
9084
}
@@ -176,7 +170,9 @@ func addConfigFields(cmd *cobra.Command, sb *output.SectionBuilder) {
176170
if viper.ConfigFileUsed() != "" {
177171
sb.Field("Config file", viper.ConfigFileUsed())
178172
}
179-
sb.Field("Log file", globals.LogPath)
173+
if globals.LogPath != "" {
174+
sb.Field("Log file", globals.LogPath)
175+
}
180176
}
181177
}
182178
}

0 commit comments

Comments
 (0)