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
25 changes: 18 additions & 7 deletions cli/cmd/analyzer_inputs.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package cmd

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

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

absPaths := make([]string, 0, len(paths))
for _, approxPath := range paths {
absApproxPath := log.AbsPathOrExit(approxPath, "dataflow-approximations")
compiledPath, err := compileApproximationsIfNeeded(absApproxPath, analyzerJarPath, projectModelDir)
if err != nil {
out.Fatalf("Approximation compilation failed: %s", err)
}
b.AddDataflowApproximations(compiledPath)
absPaths = append(absPaths, log.AbsPathOrExit(approxPath, "dataflow-approximations"))
}

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

Expand Down
74 changes: 74 additions & 0 deletions cli/cmd/approximation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cmd

import (
"fmt"

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

var initApproximationDeps []string

var approximationCmd = &cobra.Command{
Use: "approximation",
Short: "Create dataflow approximation projects",
Long: `A dataflow approximation project holds code-based models of how taint moves through
methods the analyzer cannot see into, together with the exact dependency versions those
models are written against.

Compile one with 'opentaint compile approximations', and apply it with the
--dataflow-approximations flag of 'opentaint scan' or 'opentaint test approximation run'.`,
}

var approximationInitCmd = &cobra.Command{
Use: "init <output-dir>",
Short: "Create a dataflow approximation project",
Long: `Create a Gradle project for OpenTaint dataflow approximation models.

The project includes:
- build.gradle.kts with compile-only dependencies, settings.gradle.kts
- libs/opentaint-approximations-api.jar, the @Approximate annotations and the
OpentaintNdUtil / ArgumentTypeContext support types
- src/main/java/ for the model sources, one @Approximate class per target class

Use --dependency to pin the libraries the models are written against, at the exact versions
the target application uses. Those pins are the models' compile environment: they, and
nothing about the project under analysis, decide what the models compile against.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
projectPath := log.AbsPathOrExit(args[0], "output-dir")

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

// Init replaces the build file but keeps the model sources.
// Warn the user because this removes dependency pins that they added manually.
existing := approximation.IsProject(projectPath)
if existing {
out.Warnf("Replacing the build file of the existing approximation project at %s; "+
"pass every dependency the models need, not only the new ones", projectPath)
}

if err := approximation.Scaffold(projectPath, initApproximationDeps, analyzerJarPath); err != nil {
out.Fatalf("Failed to create approximation project: %s", err)
}

if existing {
fmt.Printf("Approximation project updated at %s\n", projectPath)
} else {
fmt.Printf("Approximation project initialized at %s\n", projectPath)
}
suggest("To compile the models run", fmt.Sprintf("opentaint compile approximations %s", args[0]))
},
}

func init() {
rootCmd.AddCommand(approximationCmd)
approximationCmd.AddCommand(approximationInitCmd)

approximationInitCmd.Flags().StringArrayVar(&initApproximationDeps, "dependency", nil,
"Compile-only Maven dependency coordinates the models are written against (repeatable)")
}
10 changes: 7 additions & 3 deletions cli/cmd/command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ type BaseCommandBuilder struct {
debug bool
}

// appendVerbosityFlag passes the JAR's own --verbosity flag based on opentaint's
// debug bool. The Java tool's CLI surface is independent of opentaint's; we just
// translate. info ↔ false, debug ↔ true.
// ForceDebugVerbosity sets the tool log level to debug.
// The CLI can then read detailed output from the log file.
func (b *BaseCommandBuilder) ForceDebugVerbosity() {
b.debug = true
}

// appendVerbosityFlag maps the CLI debug setting to the tool log level.
func (b *BaseCommandBuilder) appendVerbosityFlag(flags []string) []string {
if b.debug {
return append(flags, "--verbosity=debug")
Expand Down
14 changes: 12 additions & 2 deletions cli/cmd/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,18 @@ func printCompileSummary(absOutputProjectModelPath string) {
}

func compileProject(absOutputProjectModelPath, absProjectRoot, autobuilderJarPath string, javaRunner java.JavaRunner) error {
var err error
tempLogsDir, err := os.MkdirTemp("", "opentaint-*")
if err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err)
}
tempLogsFile := filepath.Join(tempLogsDir, "autobuild.log")
return compileProjectWithLogs(absOutputProjectModelPath, absProjectRoot, autobuilderJarPath,
filepath.Join(tempLogsDir, "autobuild.log"), false, javaRunner)
}

// compileProjectWithLogs writes the autobuilder log to logsFile.
// captureBuildOutput adds the output from the build tool to this log.
func compileProjectWithLogs(absOutputProjectModelPath, absProjectRoot, autobuilderJarPath, tempLogsFile string, captureBuildOutput bool, javaRunner java.JavaRunner) error {
var err error

builder := NewAutobuilderBuilder().
SetProjectRootDir(absProjectRoot).
Expand All @@ -157,6 +163,10 @@ func compileProject(absOutputProjectModelPath, absProjectRoot, autobuilderJarPat
SetLogsFile(tempLogsFile).
SetJarPath(autobuilderJarPath)

if captureBuildOutput {
builder.ForceDebugVerbosity()
}

autobuilderCommand := builder.BuildNativeCommand()

commandSucceeded := func(_ error) bool {
Expand Down
191 changes: 55 additions & 136 deletions cli/cmd/compile_approximations.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,168 +3,87 @@ package cmd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/seqra/opentaint/internal/approximation"
"github.com/seqra/opentaint/internal/autobuilder"
"github.com/seqra/opentaint/internal/globals"
"github.com/seqra/opentaint/internal/output"
"github.com/seqra/opentaint/internal/utils"
"github.com/seqra/opentaint/internal/utils/java"
"github.com/seqra/opentaint/internal/utils/project"
)

// approxClassesJarPrefix is the path prefix under which the analyzer fat JAR
// bundles approximation support sources (OpentaintNdUtil, ArgumentTypeContext).
const approxClassesJarPrefix = "opentaint-dataflow-approximations/"

// compileApproximationsIfNeeded checks whether a --dataflow-approximations directory
// contains .java source files. If so, it compiles them using javac (with the
// analyzer JAR + project dependencies on the classpath) and returns the path to
// the compiled .class output directory. If the directory already contains only
// .class files (or no .java files at all), it is returned as-is.
//
// projectModelDir is the directory containing project.yaml — used to resolve
// project dependencies for the javac classpath (approximation code may reference
// library types like org.apache.pdfbox.pdmodel.PDDocument).
func compileApproximationsIfNeeded(approxPath string, analyzerJarPath string, projectModelDir string) (string, error) {
info, err := os.Stat(approxPath)
if err != nil {
return "", fmt.Errorf("approximation path does not exist: %w", err)
}
if !info.IsDir() {
return approxPath, nil
}

javaFiles, err := collectJavaSources(approxPath)
if err != nil {
return "", err
}
if len(javaFiles) == 0 {
return approxPath, nil
}

output.LogInfof("Found %d .java file(s) in approximations directory, compiling...", len(javaFiles))

javacPath, err := resolveJavacPath()
if err != nil {
return "", err
}
// approximationBuilder compiles a model project with the autobuilder.
// The model project supplies the compile dependencies.
type approximationBuilder struct {
analyzerJarPath string
apiJar []byte
}

extractedDir, err := os.MkdirTemp("", "opentaint-approx-deps-*")
if err != nil {
return "", fmt.Errorf("failed to create temp directory for approximation deps: %w", err)
}
defer func() { _ = os.RemoveAll(extractedDir) }()
func newApproximationBuilder(analyzerJarPath string) approximation.Builder {
return &approximationBuilder{analyzerJarPath: analyzerJarPath}
}

if err := utils.ExtractZipPrefix(analyzerJarPath, approxClassesJarPrefix, extractedDir); err != nil {
return "", fmt.Errorf("failed to extract approximation classes from analyzer JAR: %w", err)
// 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) {
autobuilder := globals.ArtifactByKind("autobuilder")
if autobuilder.Override == "" {
return "autobuilder " + autobuilder.Version, nil
}

outputDir, err := os.MkdirTemp("", "opentaint-approx-compiled-*")
// A local autobuilder has no version to compare, so compare the file itself.
info, err := os.Stat(autobuilder.Override)
if err != nil {
return "", fmt.Errorf("failed to create temp directory for compiled approximations: %w", err)
}

classpath := buildApproxClasspath(analyzerJarPath, extractedDir, projectModelDir)
if err := runJavac(javacPath, classpath, outputDir, javaFiles); err != nil {
_ = os.RemoveAll(outputDir)
return "", err
return "", fmt.Errorf("failed to read the autobuilder at %s: %w", autobuilder.Override, err)
}

output.LogInfof("Approximation compilation succeeded, output: %s", outputDir)
return outputDir, nil
return fmt.Sprintf("autobuilder %s %d %d",
autobuilder.Override, info.Size(), info.ModTime().UnixNano()), nil
}

func collectJavaSources(root string) ([]string, error) {
var javaFiles []string
err := filepath.Walk(root, func(path string, fi os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
if !fi.IsDir() && strings.HasSuffix(fi.Name(), ".java") {
javaFiles = append(javaFiles, path)
// Prepare updates the approximation API jar in the model project.
// The builder reads the jar from the analyzer only once.
func (b *approximationBuilder) Prepare(projectDir string) error {
if b.apiJar == nil {
apiJar, err := approximation.ExtractApiJar(b.analyzerJarPath)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk approximations directory: %w", err)
b.apiJar = apiJar
}
return javaFiles, nil
return approximation.WriteApiJar(projectDir, b.apiJar)
}

func resolveJavacPath() (string, error) {
javacRunner := java.NewJavaRunner().
WithSkipVerify(globals.Config.SkipVerify).
WithImageType(java.AdoptiumImageJDK).
TrySystem().
TrySpecificVersion(globals.DefaultJavaVersion)

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

javacPath := java.DeriveJavacPath(javaPath)
if _, err := os.Stat(javacPath); err != nil {
return "", fmt.Errorf("javac not found at %s (resolved from java at %s). A JDK (not JRE) is required to compile approximation sources", javacPath, javaPath)
}
return javacPath, nil
}

// buildApproxClasspath assembles the javac classpath for approximation compilation:
// 1. Analyzer JAR — contains @Approximate, @ApproximateByName annotations
// 2. Extracted approximation utilities — OpentaintNdUtil, ArgumentTypeContext
// 3. Project dependencies — library JARs that approximation code may reference
func buildApproxClasspath(analyzerJarPath, extractedDir, projectModelDir string) string {
parts := []string{analyzerJarPath, extractedDir}
parts = append(parts, resolveProjectDependencies(projectModelDir)...)
return strings.Join(parts, string(os.PathListSeparator))
}

func runJavac(javacPath, classpath, outputDir string, javaFiles []string) error {
args := []string{
"-source", "8",
"-target", "8",
"-cp", classpath,
"-d", outputDir,
javaRunner := newAutobuilderJavaRunner()
if _, err := javaRunner.EnsureJava(); err != nil {
return fmt.Errorf("failed to resolve Java for approximation compilation: %w", err)
}
args = append(args, javaFiles...)

output.LogDebugf("Running javac: %s %s", javacPath, strings.Join(args, " "))

cmd := exec.Command(javacPath, args...)
cmdOutput, cmdErr := cmd.CombinedOutput()
if cmdErr != nil {
return fmt.Errorf(
"approximation compilation failed:\n%s\njavac exited with: %w",
string(cmdOutput), cmdErr,
)
logsDir, err := os.MkdirTemp("", "opentaint-approximation-logs-*")
if err != nil {
return fmt.Errorf("failed to create temp directory for approximation build logs: %w", err)
}
return nil
}
logsFile := filepath.Join(logsDir, "approximation-build.log")

// resolveProjectDependencies reads project.yaml from the project model directory
// and returns absolute paths to the dependency JARs listed there.
func resolveProjectDependencies(projectModelDir string) []string {
if projectModelDir == "" {
return nil
// Show the spinner for direct and automatic builds.
output.LogInfof("Compiling approximation project %s", projectDir)
compile := func() error {
return compileProjectWithLogs(projectModelDir, projectDir, autobuilderJarPath, logsFile, true, javaRunner)
}
config, err := project.LoadConfig(projectModelDir)
if err != nil {
output.LogDebugf("Could not read project config for approximation compilation: %v", err)
return nil
}
var absDeps []string
for _, dep := range config.AllDependencies() {
absPath := dep
if !filepath.IsAbs(dep) {
absPath = filepath.Join(projectModelDir, dep)
}
if _, err := os.Stat(absPath); err == nil {
absDeps = append(absDeps, absPath)
if err := out.RunWithSpinner("Compiling approximation 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("failed to compile approximation project %s (build log: %s): %w", projectDir, logsFile, err)
}
output.LogDebugf("Resolved %d project dependencies for approximation classpath", len(absDeps))
return absDeps

_ = os.RemoveAll(logsDir)
return nil
}
Loading
Loading