Skip to content
Merged
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
31 changes: 22 additions & 9 deletions cli/cmd/analyzer_inputs.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
package cmd

import (
"github.com/seqra/opentaint/internal/approximation"
"github.com/seqra/opentaint/internal/models"
"github.com/seqra/opentaint/internal/utils/log"
"github.com/spf13/cobra"
)

// addDataflowApproximations resolves the paths to compiled model directories.
func addDataflowModelsFlag(cmd *cobra.Command, paths *[]string) {
cmd.Flags().StringArrayVar(paths, "dataflow-models", nil,
"Dataflow model project, build output, or class directory (repeatable)")
addDeprecatedFlagAlias(cmd.Flags(), "dataflow-models", "dataflow-approximations")
}

func addPassthroughModelsFlag(cmd *cobra.Command, paths *[]string) {
cmd.Flags().StringArrayVar(paths, "passthrough-models", nil,
"Pass-through model YAML file or directory (repeatable)")
addDeprecatedFlagAlias(cmd.Flags(), "passthrough-models", "passthrough-approximations")
}

// addDataflowModels resolves the paths to compiled model directories.
// It rebuilds a model project when its inputs change.
func addDataflowApproximations(b *AnalyzerBuilder, paths []string, analyzerJarPath string) {
func addDataflowModels(b *AnalyzerBuilder, paths []string, analyzerJarPath string) {
if len(paths) == 0 {
return
}

absPaths := make([]string, 0, len(paths))
for _, approxPath := range paths {
absPaths = append(absPaths, log.AbsPathOrExit(approxPath, "dataflow-approximations"))
for _, modelPath := range paths {
absPaths = append(absPaths, log.AbsPathOrExit(modelPath, "dataflow-models"))
}

classDirs, err := approximation.ResolveAll(absPaths, newApproximationBuilder(analyzerJarPath))
classDirs, err := models.ResolveAll(absPaths, newModelBuilder(analyzerJarPath))
if err != nil {
out.Fatalf("Failed to apply dataflow approximations: %s", err)
out.Fatalf("Failed to apply dataflow models: %s", err)
}
for _, classDir := range classDirs {
b.AddDataflowApproximations(classDir)
}
}

func addPassthroughApproximations(b *AnalyzerBuilder, paths []string) {
func addPassthroughModels(b *AnalyzerBuilder, paths []string) {
for _, passthrough := range paths {
b.AddPassthroughApproximations(log.AbsPathOrExit(passthrough, "passthrough-approximations"))
b.AddPassthroughApproximations(log.AbsPathOrExit(passthrough, "passthrough-models"))
}
}
8 changes: 8 additions & 0 deletions cli/cmd/command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ type AnalyzerBuilder struct {
debugFactReachabilitySarif bool
runRuleTests bool
debugRunAnalysisOnSelectedEntryPoints string
// logsFile is the analyzer log of the last command built.
logsFile string
}

// LogsFile returns the path of the analyzer log that the last command from BuildNativeCommand writes.
func (a *AnalyzerBuilder) LogsFile() string {
return a.logsFile
}

func (a *AnalyzerBuilder) SetProject(projectPath string) *AnalyzerBuilder {
Expand Down Expand Up @@ -193,6 +200,7 @@ func (a *AnalyzerBuilder) BuildNativeCommand() []string {
return []string{}
}
tempLogsFile := filepath.Join(tempLogsDir, "analyzer.log")
a.logsFile = tempLogsFile

command := []string{
a.maxMemory,
Expand Down
13 changes: 13 additions & 0 deletions cli/cmd/command_builder_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"os"
"path/filepath"
"reflect"
"testing"
)
Expand Down Expand Up @@ -63,6 +65,17 @@ func TestAppendVerbosityFlagPreservesExistingFlags(t *testing.T) {
}
}

// The analyzer log path is reported after an analyzer exception, so it must be the one the command writes.
func TestAnalyzerBuilderReportsItsLogsFile(t *testing.T) {
b := NewAnalyzerBuilder().SetProject("/project/project.yaml").SetOutputDir("/output")
cmd := b.BuildNativeCommand()
t.Cleanup(func() { _ = os.RemoveAll(filepath.Dir(b.LogsFile())) })

if filepath.Base(b.LogsFile()) != "analyzer.log" || !containsFlagPair(cmd, "--logs-file", b.LogsFile()) {
t.Fatalf("LogsFile() = %q, command was %v", b.LogsFile(), cmd)
}
}

// containsFlagPair reports whether args contains the adjacent pair `flag value`.
func containsFlagPair(args []string, flag, value string) bool {
for i := 0; i+1 < len(args); i++ {
Expand Down
65 changes: 0 additions & 65 deletions cli/cmd/compile_approximations_cmd.go

This file was deleted.

38 changes: 19 additions & 19 deletions cli/cmd/compile_approximations.go → cli/cmd/compile_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ import (
"os"
"path/filepath"

"github.com/seqra/opentaint/internal/approximation"
"github.com/seqra/opentaint/internal/autobuilder"
"github.com/seqra/opentaint/internal/globals"
"github.com/seqra/opentaint/internal/models"
"github.com/seqra/opentaint/internal/output"
)

// approximationBuilder compiles a model project with the autobuilder.
// modelBuilder compiles a model project with the autobuilder.
// The model project supplies the compile dependencies.
type approximationBuilder struct {
type modelBuilder struct {
analyzerJarPath string
apiJar []byte
}

func newApproximationBuilder(analyzerJarPath string) approximation.Builder {
return &approximationBuilder{analyzerJarPath: analyzerJarPath}
func newModelBuilder(analyzerJarPath string) models.Builder {
return &modelBuilder{analyzerJarPath: analyzerJarPath}
}

// Fingerprint identifies the compiler a build was produced with.
// The source stamp covers the model project but not the tool that reads it, so without
// this an upgraded CLI keeps serving models compiled by the previous one.
func (b *approximationBuilder) Fingerprint() (string, error) {
func (b *modelBuilder) Fingerprint() (string, error) {
autobuilder := globals.ArtifactByKind("autobuilder")
if autobuilder.Override == "" {
return "autobuilder " + autobuilder.Version, nil
Expand All @@ -40,48 +40,48 @@ func (b *approximationBuilder) Fingerprint() (string, error) {
autobuilder.Override, info.Size(), info.ModTime().UnixNano()), nil
}

// Prepare updates the approximation API jar in the model project.
// Prepare updates the model API jar in the model project.
// The builder reads the jar from the analyzer only once.
func (b *approximationBuilder) Prepare(projectDir string) error {
func (b *modelBuilder) Prepare(projectDir string) error {
if b.apiJar == nil {
apiJar, err := approximation.ExtractApiJar(b.analyzerJarPath)
apiJar, err := models.ExtractApiJar(b.analyzerJarPath)
if err != nil {
return err
}
b.apiJar = apiJar
}
return approximation.WriteApiJar(projectDir, b.apiJar)
return models.WriteApiJar(projectDir, b.apiJar)
}

func (b *approximationBuilder) Compile(projectDir, projectModelDir string) error {
func (b *modelBuilder) Compile(projectDir, projectModelDir string) error {
autobuilderJarPath, err := ensureAutobuilderAvailable()
if err != nil {
return fmt.Errorf("failed to resolve autobuilder: %w", err)
}

javaRunner := newAutobuilderJavaRunner()
if _, err := javaRunner.EnsureJava(); err != nil {
return fmt.Errorf("failed to resolve Java for approximation compilation: %w", err)
return fmt.Errorf("failed to resolve Java for model compilation: %w", err)
}

logsDir, err := os.MkdirTemp("", "opentaint-approximation-logs-*")
logsDir, err := os.MkdirTemp("", "opentaint-model-logs-*")
if err != nil {
return fmt.Errorf("failed to create temp directory for approximation build logs: %w", err)
return fmt.Errorf("failed to create temp directory for model build logs: %w", err)
}
logsFile := filepath.Join(logsDir, "approximation-build.log")
logsFile := filepath.Join(logsDir, "model-build.log")

// Show the spinner for direct and automatic builds.
output.LogInfof("Compiling approximation project %s", projectDir)
output.LogInfof("Compiling model project %s", projectDir)
compile := func() error {
return compileProjectWithLogs(projectModelDir, projectDir, autobuilderJarPath, logsFile, true, javaRunner)
}
if err := out.RunWithSpinner("Compiling approximation project "+filepath.Base(projectDir), compile); err != nil {
if err := out.RunWithSpinner("Compiling model project "+filepath.Base(projectDir), compile); err != nil {
// Show compiler errors when they are available.
// Keep the full log if no compiler error is available.
if diagnostics := autobuilder.CompilerDiagnostics(logsFile); diagnostics != "" {
return fmt.Errorf("approximation project %s failed to compile:\n%s", projectDir, diagnostics)
return fmt.Errorf("model project %s failed to compile:\n%s", projectDir, diagnostics)
}
return fmt.Errorf("failed to compile approximation project %s (build log: %s): %w", projectDir, logsFile, err)
return fmt.Errorf("failed to compile model project %s (build log: %s): %w", projectDir, logsFile, err)
}

_ = os.RemoveAll(logsDir)
Expand Down
66 changes: 66 additions & 0 deletions cli/cmd/compile_models_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cmd

import (
"fmt"

"github.com/seqra/opentaint/internal/models"
"github.com/seqra/opentaint/internal/utils/log"
"github.com/spf13/cobra"
)

var compileModelsOutput string

var compileModelsCmd = &cobra.Command{
Use: "models <model-project>",
Short: "Compile a dataflow model project",
Long: `Compile the dataflow models in <model-project> against the
dependencies that project pins in its own build file.

This is the model project, not the project to scan: its build.gradle.kts lists the exact
library versions the models are written against, so the same models compile identically
wherever they are applied.

Scanning and testing build a model project on demand, so running this is only
needed to compile ahead of time or to see compilation errors on their own.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
projectPath := log.AbsPathOrExit(args[0], "model-project")

if !models.IsProject(projectPath) {
out.Fatalf("%s is not a model project: it has no build.gradle.kts declaring the "+
"dependencies its models are written against.\nCreate one with 'opentaint model init %s "+
"--dependency <group:artifact:version>'", projectPath, args[0])
}

outputPath := compileModelsOutput
if outputPath == "" {
outputPath = models.BuildDir(projectPath)
} else {
outputPath = log.AbsPathOrExit(outputPath, "output")
}

analyzerJarPath, err := ensureAnalyzerAvailable()
if err != nil {
out.Fatalf("Failed to resolve analyzer: %s", err)
}

if err := models.Build(projectPath, outputPath, newModelBuilder(analyzerJarPath)); err != nil {
out.Fatalf("Model compilation failed: %s", err)
}

out.Section("Model Compile Summary").
Field("Model project", projectPath).
Field("Compiled models written to", models.ClassesDir(outputPath)).
Render()
suggest("To apply these models in a scan run",
fmt.Sprintf("opentaint scan <project-model> --dataflow-models %s", args[0]))
},
}

func init() {
compileCmd.AddCommand(compileModelsCmd)

compileModelsCmd.Flags().StringVarP(&compileModelsOutput, "output", "o", "",
"Path to the compiled models (default: <model-project>/.opentaint/build)")
compileCmd.AddCommand(deprecatedCommand(compileModelsCmd, "approximations", "opentaint compile models"))
}
37 changes: 37 additions & 0 deletions cli/cmd/deprecated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cmd

import (
"strings"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// deprecatedCommand returns a hidden copy of cmd under its old name. Running it prints that the
// name is deprecated, pointing at newPath, and then runs cmd with the same flags.
func deprecatedCommand(cmd *cobra.Command, oldName, newPath string) *cobra.Command {
alias := &cobra.Command{
Use: oldName + strings.TrimPrefix(cmd.Use, cmd.Name()),
Short: cmd.Short,
Long: cmd.Long,
Args: cmd.Args,
Run: cmd.Run,
Annotations: cmd.Annotations,
Deprecated: "use '" + newPath + "' instead",
}
alias.Flags().AddFlagSet(cmd.Flags())
return alias
}

// addDeprecatedFlagAlias registers oldName as a hidden, deprecated name of the flag newName.
// Both names set the same value, so they can be mixed on one command line.
func addDeprecatedFlagAlias(flags *pflag.FlagSet, newName, oldName string) {
flag := flags.Lookup(newName)
flags.AddFlag(&pflag.Flag{
Name: oldName,
Usage: flag.Usage,
Value: flag.Value,
DefValue: flag.DefValue,
})
_ = flags.MarkDeprecated(oldName, "use --"+newName+" instead")
}
Loading
Loading