|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "os/signal" |
| 7 | + "path/filepath" |
| 8 | + "syscall" |
| 9 | + |
| 10 | + "github.com/spf13/cobra" |
| 11 | + |
| 12 | + "github.com/tronprotocol/tron-deployment/internal/build" |
| 13 | + "github.com/tronprotocol/tron-deployment/internal/output" |
| 14 | +) |
| 15 | + |
| 16 | +// `trond build` produces a deployable java-tron artifact (JAR in |
| 17 | +// Phase 1; image lands in Phase 3) from a source tree, by running |
| 18 | +// gradle inside a pinned Eclipse Temurin container. |
| 19 | +// |
| 20 | +// Design + rationale: specs/002-trond-build-pipeline/{spec,plan}.md. |
| 21 | +// |
| 22 | +// Output schema: schemas/output/build.schema.json. |
| 23 | + |
| 24 | +var ( |
| 25 | + buildSourcePath string |
| 26 | + buildRevisionSpec string |
| 27 | + buildArtifactKind string |
| 28 | + buildJDKVersion string |
| 29 | + buildGradleTask string |
| 30 | + buildGradleArgs []string |
| 31 | + buildBuilder string |
| 32 | + buildImageTag string |
| 33 | + buildImageOverride string |
| 34 | +) |
| 35 | + |
| 36 | +var buildCmd = &cobra.Command{ |
| 37 | + Use: "build", |
| 38 | + Short: "Build a java-tron artifact (JAR or image) from source", |
| 39 | + Long: `Build runs gradle inside a pinned Eclipse Temurin container against |
| 40 | +the given java-tron source tree, producing either a fat JAR or a |
| 41 | +docker image. Results are content-addressed by git revision + builder |
| 42 | +image digest + task + args, so repeated invocations against the same |
| 43 | +inputs return immediately. |
| 44 | +
|
| 45 | +trond ships no JDK or Gradle. The builder image is pulled on first |
| 46 | +use and pinned via go:embed so the build is reproducible across |
| 47 | +trond installs of the same version. |
| 48 | +
|
| 49 | +Examples: |
| 50 | +
|
| 51 | + # Build the default fat JAR from the current branch HEAD. |
| 52 | + trond build --source ./java-tron --artifact jar -o json |
| 53 | +
|
| 54 | + # Build with an explicit revision and gradle flags. |
| 55 | + trond build --source ./java-tron --revision v4.7.7 \ |
| 56 | + --gradle-arg=--offline --gradle-arg=-Dversion=mytest -o json |
| 57 | +
|
| 58 | + # Override the builder image (emergency: pinned digest unreachable). |
| 59 | + trond build --source ./java-tron \ |
| 60 | + --builder-image-override eclipse-temurin:8-jdk@sha256:abcd...`, |
| 61 | + RunE: runBuild, |
| 62 | +} |
| 63 | + |
| 64 | +func init() { |
| 65 | + buildCmd.Flags().StringVar(&buildSourcePath, "source", "", |
| 66 | + "Path to the java-tron source tree (required; relative to CWD)") |
| 67 | + buildCmd.Flags().StringVar(&buildRevisionSpec, "revision", "HEAD", |
| 68 | + "Git revision to build (HEAD, branch, tag, or sha)") |
| 69 | + buildCmd.Flags().StringVar(&buildArtifactKind, "artifact", "jar", |
| 70 | + "Artifact kind: 'jar' or 'image'") |
| 71 | + buildCmd.Flags().StringVar(&buildJDKVersion, "jdk", "8", |
| 72 | + "JDK version for the builder container (8|11|17|21)") |
| 73 | + buildCmd.Flags().StringVar(&buildGradleTask, "gradle-task", "", |
| 74 | + "Gradle task name (defaults: 'shadowJar' for jar, 'dockerBuild' for image)") |
| 75 | + buildCmd.Flags().StringArrayVar(&buildGradleArgs, "gradle-arg", nil, |
| 76 | + "Extra gradle args (repeatable; e.g. --gradle-arg=--offline). "+ |
| 77 | + "Restricted to a flag-name allowlist; see spec FR-022.") |
| 78 | + buildCmd.Flags().StringVar(&buildBuilder, "builder", "docker", |
| 79 | + "Builder backend: 'docker' (default) or 'host' (uses local gradle)") |
| 80 | + buildCmd.Flags().StringVar(&buildImageTag, "tag", "", |
| 81 | + "Image tag to apply when --artifact=image (e.g. mytest:dev)") |
| 82 | + buildCmd.Flags().StringVar(&buildImageOverride, "builder-image-override", "", |
| 83 | + "Override the pinned builder image (escape hatch; see FR-024)") |
| 84 | + rootCmd.AddCommand(buildCmd) |
| 85 | +} |
| 86 | + |
| 87 | +// runBuild wires CLI flags into a build.Request, installs the |
| 88 | +// signal-aware context for SIGINT propagation (FR-016), and emits |
| 89 | +// either the success Result or a structured error envelope. |
| 90 | +func runBuild(cmd *cobra.Command, _ []string) error { |
| 91 | + // FR-021: --source relative to CWD. |
| 92 | + resolvedSource := buildSourcePath |
| 93 | + if resolvedSource != "" && !filepath.IsAbs(resolvedSource) { |
| 94 | + abs, err := filepath.Abs(resolvedSource) |
| 95 | + if err == nil { |
| 96 | + resolvedSource = abs |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + req := build.Request{ |
| 101 | + SourcePath: resolvedSource, |
| 102 | + RevisionSpec: buildRevisionSpec, |
| 103 | + JDKVersion: buildJDKVersion, |
| 104 | + ArtifactKind: buildArtifactKind, |
| 105 | + GradleTask: buildGradleTask, |
| 106 | + GradleArgs: buildGradleArgs, |
| 107 | + Builder: buildBuilder, |
| 108 | + ImageTag: buildImageTag, |
| 109 | + BuilderImageOverride: buildImageOverride, |
| 110 | + } |
| 111 | + |
| 112 | + // SIGINT-aware context. Build container + git subprocesses all |
| 113 | + // run under this; cancellation propagates to subprocess kill. |
| 114 | + ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) |
| 115 | + defer cancel() |
| 116 | + |
| 117 | + res, err := build.Run(ctx, req) |
| 118 | + if err != nil { |
| 119 | + // StructuredError propagates through cobra RunE; cmd.Execute |
| 120 | + // renders it and sets the exit code (see cmd/root.go::Execute). |
| 121 | + // Returning here lets `defer cancel()` run normally. |
| 122 | + return err |
| 123 | + } |
| 124 | + |
| 125 | + outputFmt, _ := cmd.Flags().GetString("output") |
| 126 | + if outputFmt == "json" { |
| 127 | + return output.WriteJSON(os.Stdout, res) |
| 128 | + } |
| 129 | + if res.CacheHit { |
| 130 | + fmt.Printf("✓ cache hit: %s (%d ms)\n", res.CacheKey, res.DurationMs) |
| 131 | + } else { |
| 132 | + fmt.Printf("✓ built: %s\n → %s\n sha256: %s\n %d ms\n", |
| 133 | + res.CacheKey, res.ArtifactPath, res.SHA256, res.DurationMs) |
| 134 | + } |
| 135 | + return nil |
| 136 | +} |
0 commit comments