diff --git a/build/env/env.go b/build/env/env.go index 75356fc2..81078fa7 100644 --- a/build/env/env.go +++ b/build/env/env.go @@ -6,6 +6,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "github.com/oasisprotocol/cli/cmd/common" ) @@ -86,6 +87,10 @@ type ContainerEnv struct { } var containerCmds = []string{"docker", "podman"} +var ( + containerCmdPath string + containerCmdOnce sync.Once +) // NewContainerEnv creates a new Docker or Podman-based execution environment. func NewContainerEnv(image, baseDir, dirMount string) *ContainerEnv { @@ -213,12 +218,20 @@ func (de *ContainerEnv) HasBinary(string) bool { // getContainerCmd finds a working docker or podman command and returns its path. func getContainerCmd() string { - for _, cmd := range containerCmds { - if path, err := exec.LookPath(cmd); err == nil && path != "" { - return path + containerCmdOnce.Do(func() { + for _, cmd := range containerCmds { + if path, err := exec.LookPath(cmd); err == nil && path != "" { + containerCmdPath = path + return + } } - } - return "" + }) + return containerCmdPath +} + +// IsContainerRuntimeAvailable returns true if a container runtime (docker or podman) is available. +func IsContainerRuntimeAvailable() bool { + return getContainerCmd() != "" } // IsAvailable implements ExecEnv. diff --git a/build/rofl/artifacts.go b/build/rofl/artifacts.go index 8ec04d9c..e4288e19 100644 --- a/build/rofl/artifacts.go +++ b/build/rofl/artifacts.go @@ -1,5 +1,8 @@ package rofl +// DefaultBuilderImage is the default builder container image to use when building ROFL apps. +const DefaultBuilderImage = "ghcr.io/oasisprotocol/rofl-dev:v0.5.0@sha256:31573686552abeb0edebc450f6872831f0006a6cf38220cef7e0789d4376c2c1" + // LatestBasicArtifacts are the latest TDX ROFL basic app artifacts. var LatestBasicArtifacts = ArtifactsConfig{ Firmware: "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.6.2/ovmf.tdx.fd#db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f", diff --git a/build/sgxs/sgxs.go b/build/sgxs/sgxs.go index 5ef4fceb..e5552f2a 100644 --- a/build/sgxs/sgxs.go +++ b/build/sgxs/sgxs.go @@ -15,10 +15,10 @@ import ( // It requires the `ftxsgx-elf2sgxs` utility to be installed. func Elf2Sgxs(buildEnv env.ExecEnv, elfSgxPath, sgxsPath string, heapSize, stackSize, threads uint64) (err error) { if elfSgxPath, err = buildEnv.PathToEnv(elfSgxPath); err != nil { - return + return err } if sgxsPath, err = buildEnv.PathToEnv(sgxsPath); err != nil { - return + return err } args := []string{ @@ -31,7 +31,7 @@ func Elf2Sgxs(buildEnv env.ExecEnv, elfSgxPath, sgxsPath string, heapSize, stack cmd := exec.Command("ftxsgx-elf2sgxs", args...) if err = buildEnv.WrapCommand(cmd); err != nil { - return + return err } if common.IsVerbose() { fmt.Println(cmd) diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index 79c95c36..045a153a 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -7,7 +7,9 @@ import ( "maps" "os" "os/exec" + "runtime" "slices" + "strings" "github.com/spf13/cobra" flag "github.com/spf13/pflag" @@ -88,41 +90,56 @@ var ( // Ensure deterministic umask for builds. setUmask(0o002) + // Determine builder image to use. + builderImage := "" + if manifest.Artifacts != nil { + builderImage = strings.TrimSpace(manifest.Artifacts.Builder) + if manifest.Artifacts.Builder != "" && builderImage == "" { + return fmt.Errorf("builder image is empty after trimming whitespace") + } + } + + // Determine if we need to use a container for building. + // Native builds are only supported on Linux. On other platforms, we require + // a container. + nativeBuildSupported := runtime.GOOS == "linux" && runtime.GOARCH == "amd64" + containerAvailable := env.IsContainerRuntimeAvailable() + var buildEnv env.ExecEnv switch { - case manifest.Artifacts == nil || manifest.Artifacts.Builder == "" || noContainer: + case noContainer: + if !nativeBuildSupported { + return fmt.Errorf("native ROFL builds are only supported on linux/amd64; remove --no-container to use containerized builds on %s/%s", runtime.GOOS, runtime.GOARCH) + } + // Force native build regardless of manifest. buildEnv = env.NewNativeEnv() default: - var baseDir string - baseDir, err = env.GetBasedir() - if err != nil { - return fmt.Errorf("failed to determine base directory: %w", err) + useContainer := false + if !nativeBuildSupported { + useContainer = true + } else if builderImage != "" { + useContainer = true } - containerEnv := env.NewContainerEnv( - manifest.Artifacts.Builder, - baseDir, - "/src", - ) - containerEnv.AddDirectory(tmpDir) - buildEnv = containerEnv - - if buildEnv.IsAvailable() { - fmt.Printf("Initializing build environment...\n") - // Run a dummy command to make sure that all necessary Docker layers - // for the build environment are downloaded at the start instead of - // later in the build process. - // Also pipe all output from the process to stdout/stderr, so the user - // can follow the progress in real-time. - cmd := exec.Command("true") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err = buildEnv.WrapCommand(cmd); err != nil { - return fmt.Errorf("unable to wrap command: %w", err) - } - if err = cmd.Run(); err != nil { - return fmt.Errorf("failed to initialize build environment: %w", err) + if !useContainer { + buildEnv = env.NewNativeEnv() + break + } + + if !containerAvailable { + if builderImage != "" { + return fmt.Errorf("builder specified in manifest but no container runtime (docker or podman) is available") } + return fmt.Errorf("native ROFL builds are only supported on linux/amd64; on %s/%s you need docker or podman installed", runtime.GOOS, runtime.GOARCH) + } + + if builderImage == "" { + builderImage = buildRofl.DefaultBuilderImage + } + fmt.Printf("Using container build environment (image: %s)\n", builderImage) + buildEnv, err = setupContainerEnv(builderImage, tmpDir) + if err != nil { + return err } } @@ -304,6 +321,39 @@ var ( } ) +// setupContainerEnv creates and initializes a container build environment. +func setupContainerEnv(builderImage, tmpDir string) (env.ExecEnv, error) { + baseDir, err := env.GetBasedir() + if err != nil { + return nil, fmt.Errorf("failed to determine base directory: %w", err) + } + + containerEnv := env.NewContainerEnv( + builderImage, + baseDir, + "/src", + ) + containerEnv.AddDirectory(tmpDir) + + fmt.Printf("Initializing build environment...\n") + // Run a dummy command to make sure that all necessary Docker layers + // for the build environment are downloaded at the start instead of + // later in the build process. + // Also pipe all output from the process to stdout/stderr, so the user + // can follow the progress in real-time. + cmd := exec.Command("true") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err = containerEnv.WrapCommand(cmd); err != nil { + return nil, fmt.Errorf("unable to wrap command: %w", err) + } + if err = cmd.Run(); err != nil { + return nil, fmt.Errorf("failed to initialize build environment with image %s (ensure the image is accessible and your container runtime can pull it): %w", builderImage, err) + } + + return containerEnv, nil +} + func setupBuildEnv(deployment *buildRofl.Deployment, npa *common.NPASelection) { // Configure app ID. os.Setenv("ROFL_APP_ID", deployment.AppID) diff --git a/docs/rofl.md b/docs/rofl.md index f4b08d0e..ae6ec6b8 100644 --- a/docs/rofl.md +++ b/docs/rofl.md @@ -98,6 +98,16 @@ Building ROFL apps does not require a working TEE on your machine. However, you do need to install all corresponding tools. Check out the [ROFL Prerequisites] chapter for details. +Platform/container selection rules: + +- On Linux/amd64, builds are native by default unless the manifest specifies a + `builder` image; you can force native builds with `--no-container` even if a + `builder` is set. +- On macOS/Windows/other architectures, builds always use a containerized + builder; `--no-container` will fail on these platforms. +- If a containerized build is selected (due to platform or manifest), Docker or + Podman must be available; otherwise the build fails. + ::: [ROFL Prerequisites]: https://github.com/oasisprotocol/oasis-sdk/blob/main/docs/rofl/workflow/prerequisites.md