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
17 changes: 14 additions & 3 deletions targets/windows/handle_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,20 @@ func handleContainer(ctx context.Context, client gwclient.Client) (*gwclient.Res
platform = &defaultPlatform
}
baseImage := bi.ToState(sOpt, pg, llb.Platform(*platform))
out := baseImage.
File(llb.Copy(bin, "/", windowsSystemDir), pg).
With(copySymlinks(spec.GetImagePost(targetKey), pg))

// Install every package's binaries (primary + supplemental) into the
// image, matching the linux container target which installs all packages
// produced for the target. Always copy the primary package so the build
// step is realized even when it has no binaries.
pkgs := windowsPackages(spec, targetKey)
Comment thread
cpuguy83 marked this conversation as resolved.
out := baseImage
for _, pkg := range pkgs {
if !pkg.Primary && len(pkg.Binaries) == 0 {
continue
}
out = out.File(llb.Copy(bin, pkg.internalDir(), windowsSystemDir, dalec.WithDirContentsOnly()), pg)
}
out = out.With(copySymlinks(spec.GetImagePost(targetKey), pg))

def, err := out.Marshal(ctx)
if err != nil {
Expand Down
135 changes: 116 additions & 19 deletions targets/windows/handle_zip.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, e
return nil, nil, err
}

if err := validateZipArtifacts(spec, targetKey); err != nil {
return nil, nil, err
}

pg := dalec.ProgressGroup("Build windows container: " + spec.Name)
worker := distroConfig.Worker(sOpt, pg)

bin := buildBinaries(ctx, spec, worker, client, sOpt, targetKey, pg)

st := getZipLLB(worker, platform, spec, bin, pg)
st := getZipLLB(worker, platform, spec, targetKey, bin, pg)

def, err := st.Marshal(ctx)
if err != nil {
Expand Down Expand Up @@ -167,6 +171,8 @@ func buildBinaries(ctx context.Context, spec *dalec.Spec, worker llb.State, clie
return dalec.ErrorState(worker, err)
}

packageOpts := append([]llb.ConstraintsOpt(nil), opts...)

// Apply source map constraints for build steps
opts = append(opts, spec.Build.Steps.GetSourceLocation(worker))

Expand All @@ -177,11 +183,11 @@ func buildBinaries(ctx context.Context, spec *dalec.Spec, worker llb.State, clie

patched := dalec.PatchSources(worker, spec, sources, opts...)
buildScript := createBuildScript(spec, opts...)
artifacts := spec.GetArtifacts(targetKey)
script := generateInvocationScript(artifacts.Binaries)
script := generateInvocationScript(spec, targetKey)
pkgs := windowsPackages(spec, targetKey)

builder := worker.With(dalec.SetBuildNetworkMode(spec))
st := builder.Run(
runOpts := []llb.RunOption{
dalec.ShArgs(script.String()),
llb.Dir("/build"),
withSourcesMounted("/build", patched, spec.Sources, opts...),
Expand All @@ -203,37 +209,128 @@ func buildBinaries(ctx context.Context, spec *dalec.Spec, worker llb.State, clie
ei.State = ei.State.With(llb.AddEnv(k, v))
}
}),
).AddMount(outputDir, llb.Scratch())
}
for _, pkg := range pkgs {
runOpts = append(runOpts, llb.AddMount(pkg.buildOutputDir(), llb.Scratch()))
}

built := builder.Run(runOpts...)
packageStates := make([]llb.State, 0, len(pkgs))
for _, pkg := range pkgs {
original := built.GetMount(pkg.buildOutputDir())
signed := frontend.MaybeSign(ctx, client, original, spec, targetKey, sOpt, packageOpts...)

// Signers may return only changed files. Preserve the original package
// and overlay the signer output so replacements win.
packageState := original.File(llb.Copy(signed, "/", "/"), packageOpts...)
packageStates = append(packageStates, llb.Scratch().File(
llb.Copy(packageState, "/", pkg.internalDir(), dalec.WithCreateDestPath()),
packageOpts...,
))
}

return frontend.MaybeSign(ctx, client, st, spec, targetKey, sOpt)
return dalec.MergeAtPath(llb.Scratch(), packageStates, "/", packageOpts...)
}

func getZipLLB(worker llb.State, platform *ocispecs.Platform, spec *dalec.Spec, artifacts llb.State, opts ...llb.ConstraintsOpt) llb.State {
fileName := fmt.Sprintf("%s_%s-%s_%s.zip", spec.Name, spec.Version, spec.Revision, platform.Architecture)
outName := filepath.Join(outputDir, fileName)
func getZipLLB(worker llb.State, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string, artifacts llb.State, opts ...llb.ConstraintsOpt) llb.State {
const artifactsDir = "/tmp/artifacts"

// Each package is zipped into a separate
// "<name>_<version>-<revision>_<arch>.zip" file.
script := &strings.Builder{}
fmt.Fprintln(script, "set -ex")
for _, pkg := range windowsPackages(spec, targetKey) {
Comment thread
cpuguy83 marked this conversation as resolved.
fileName := fmt.Sprintf("%s_%s-%s_%s.zip", pkg.Name, spec.Version, spec.Revision, platform.Architecture)
outName := filepath.Join(outputDir, fileName)
srcDir := path.Join(artifactsDir, pkg.internalDir())
fmt.Fprintf(script, "(cd %q && find . -maxdepth 1 -type f -exec zip %q {} +)\n", srcDir, outName)
}

zipped := worker.Run(
dalec.ShArgs("zip "+outName+" *"),
llb.Dir("/tmp/artifacts"),
llb.AddMount("/tmp/artifacts", artifacts),
dalec.ShArgs(script.String()),
llb.AddMount(artifactsDir, artifacts),
dalec.WithConstraints(opts...),
).AddMount(outputDir, llb.Scratch())
return zipped
}

func generateInvocationScript(binaries map[string]dalec.ArtifactConfig) *strings.Builder {
// windowsPackage pairs a resolved package name with the binary artifacts that
// go into it for a windows target.
type windowsPackage struct {
// Index is the package's deterministic position: primary first, then
// supplemental packages sorted by map key.
Index int
// Name is the resolved package name (primary package name or the
// supplemental package's resolved name).
Name string
// Binaries are the binary artifacts that belong to this package.
Binaries map[string]dalec.ArtifactConfig
// Primary is true for the spec's primary package.
Primary bool
}

func (p windowsPackage) buildOutputDir() string {
return path.Join(outputDir, fmt.Sprintf("%d", p.Index))
}

func (p windowsPackage) internalDir() string {
return path.Join("/", fmt.Sprintf("%d", p.Index))
}

// windowsPackages returns the ordered set of packages produced for a windows
// target: the primary package first, followed by supplemental packages sorted
// by their map key. Windows packages only ship binaries.
func windowsPackages(spec *dalec.Spec, targetKey string) []windowsPackage {
pkgs := []windowsPackage{{
Index: 0,
Name: spec.Name,
Binaries: spec.GetArtifacts(targetKey).Binaries,
Primary: true,
}}

for key, p := range dalec.GetSubPackagesForTarget(spec, targetKey) {
var binaries map[string]dalec.ArtifactConfig
if p.Artifacts != nil {
binaries = p.Artifacts.Binaries
}
pkgs = append(pkgs, windowsPackage{
Index: len(pkgs),
Name: p.ResolvedName(spec.Name, key),
Binaries: binaries,
})
}

return pkgs
}

func generateInvocationScript(spec *dalec.Spec, targetKey string) *strings.Builder {
script := &strings.Builder{}
fmt.Fprintln(script, "#!/usr/bin/env sh")
fmt.Fprintln(script, "set -ex")
fmt.Fprintf(script, "/tmp/scripts/%s\n", buildScriptName)
sorted := dalec.SortMapKeys(binaries)
for _, bin := range sorted {
config := binaries[bin]
fmt.Fprintf(script, "mv '%s' '%s'\n", bin, outputDir)

// Each output path is a separate scratch mount, so every package state has
// the flat root expected by signers. Files are copied (not moved) so a build
// output shared by multiple packages stays available.
for _, pkg := range windowsPackages(spec, targetKey) {
writePackageArtifacts(script, pkg.buildOutputDir(), pkg)
}

return script
}

// writePackageArtifacts writes the commands that stage a single package's
// binaries under destDir.
func writePackageArtifacts(script *strings.Builder, destDir string, pkg windowsPackage) {
fmt.Fprintf(script, "mkdir -p '%s'\n", destDir)

for bin, config := range dalec.SortedMapIter(pkg.Binaries) {
dest := path.Join(destDir, config.ResolveName(bin))
fmt.Fprintf(script, "cp -r '%s' '%s'\n", bin, dest)
if config.Permissions.Perm() != 0 {
fmt.Fprintf(script, "chmod %o '%s/%s'\n", config.Permissions.Perm(), outputDir, bin)
fmt.Fprintf(script, "chmod %o '%s'\n", config.Permissions.Perm(), dest)
}
}
return script
}

func createBuildScript(spec *dalec.Spec, opts ...llb.ConstraintsOpt) llb.State {
Expand Down
152 changes: 152 additions & 0 deletions targets/windows/handle_zip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package windows

import (
"strings"
"testing"

"github.com/project-dalec/dalec"
"gotest.tools/v3/assert"
)

const testTargetKey = "windowscross"

func subpackageSpec() *dalec.Spec {
return &dalec.Spec{
Name: "foo",
Version: "1.0.0",
Artifacts: dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"bin/foo.exe": {},
},
},
Targets: map[string]dalec.Target{
testTargetKey: {
Packages: map[string]dalec.SubPackage{
"contrib": {
Description: "Contrib extras for foo",
Artifacts: &dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"bin/foo-contrib.exe": {},
},
},
},
"tools": {
Name: "foo-utilities",
Description: "Foo utilities",
Artifacts: &dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"bin/util.exe": {Name: "foo-util.exe"},
},
},
},
},
},
},
}
}

func TestWindowsPackages(t *testing.T) {
t.Run("packages use deterministic indices independent of their names", func(t *testing.T) {
spec := subpackageSpec()
pkgs := windowsPackages(spec, testTargetKey)

assert.Equal(t, len(pkgs), 3)
assert.Equal(t, pkgs[0].Name, "foo")
assert.Equal(t, pkgs[1].Name, "foo-contrib")
assert.Equal(t, pkgs[2].Name, "foo-utilities")
assert.Equal(t, pkgs[0].internalDir(), "/0")
assert.Equal(t, pkgs[1].internalDir(), "/1")
assert.Equal(t, pkgs[2].internalDir(), "/2")
assert.Equal(t, len(pkgs[0].Binaries), 1)
assert.Equal(t, len(pkgs[2].Binaries), 1)
})
}

func TestGenerateInvocationScript(t *testing.T) {
t.Run("each package is staged into an independent flat output", func(t *testing.T) {
spec := subpackageSpec()
script := generateInvocationScript(spec, testTargetKey).String()

assert.Assert(t, strings.Contains(script, "mkdir -p '"+outputDir+"/0'"))
assert.Assert(t, strings.Contains(script, "mkdir -p '"+outputDir+"/1'"))
assert.Assert(t, strings.Contains(script, "mkdir -p '"+outputDir+"/2'"))
assert.Assert(t, strings.Contains(script, "cp -r 'bin/foo.exe' '"+outputDir+"/0/foo.exe'"))
assert.Assert(t, strings.Contains(script, "cp -r 'bin/foo-contrib.exe' '"+outputDir+"/1/foo-contrib.exe'"))
assert.Assert(t, strings.Contains(script, "cp -r 'bin/util.exe' '"+outputDir+"/2/foo-util.exe'"))
assert.Assert(t, strings.Contains(script, buildScriptName))
})
}

func TestGenerateInvocationScriptPermissions(t *testing.T) {
spec := &dalec.Spec{
Name: "foo",
Artifacts: dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"sub/dir/foo.exe": {Permissions: 0o755},
},
},
}

script := generateInvocationScript(spec, testTargetKey).String()

// chmod must target the staged file (by resolved name), not the original
// build path. This guards against the previous bug where chmod referenced
// the source path under the output dir. The primary package stages at the
// output dir root.
assert.Assert(t, strings.Contains(script, "cp -r 'sub/dir/foo.exe' '"+outputDir+"/0/foo.exe'"))
assert.Assert(t, strings.Contains(script, "chmod 755 '"+outputDir+"/0/foo.exe'"))
}

func TestValidateRuntimeDeps(t *testing.T) {
t.Run("a supplemental package runtime dependency identifies the package", func(t *testing.T) {
spec := subpackageSpec()
target := spec.Targets[testTargetKey]
pkg := target.Packages["contrib"]
pkg.Dependencies = &dalec.SubPackageDependencies{
Runtime: dalec.PackageDependencyList{"distinctive-runtime-dependency": {}},
}
target.Packages["contrib"] = pkg
spec.Targets[testTargetKey] = target

err := validateRuntimeDeps(spec, testTargetKey)
assert.ErrorContains(t, err, `package "foo-contrib"`)
assert.ErrorContains(t, err, "cannot have runtime dependencies")
})

t.Run("supplemental runtime dependencies do not affect zip validation", func(t *testing.T) {
spec := subpackageSpec()
target := spec.Targets[testTargetKey]
pkg := target.Packages["contrib"]
pkg.Dependencies = &dalec.SubPackageDependencies{
Runtime: dalec.PackageDependencyList{"distinctive-runtime-dependency": {}},
}
target.Packages["contrib"] = pkg
spec.Targets[testTargetKey] = target

assert.NilError(t, validateZipArtifacts(spec, testTargetKey))
})
}

func TestValidateZipArtifacts(t *testing.T) {
t.Run("all packages have artifacts", func(t *testing.T) {
spec := subpackageSpec()
assert.NilError(t, validateZipArtifacts(spec, testTargetKey))
})

t.Run("empty subpackage", func(t *testing.T) {
spec := subpackageSpec()
tgt := spec.Targets[testTargetKey]
tgt.Packages["empty"] = dalec.SubPackage{Description: "no artifacts"}
spec.Targets[testTargetKey] = tgt

err := validateZipArtifacts(spec, testTargetKey)
assert.ErrorContains(t, err, "foo-empty")
assert.ErrorContains(t, err, "no artifacts")
})

t.Run("empty primary", func(t *testing.T) {
spec := &dalec.Spec{Name: "foo"}
err := validateZipArtifacts(spec, testTargetKey)
assert.ErrorContains(t, err, "\"foo\"")
})
}
Loading
Loading