targets/windows/handle_zip.go (172 lines of code) (raw):
package windows
import (
"bytes"
"context"
"fmt"
"path/filepath"
"strings"
"github.com/Azure/dalec"
"github.com/Azure/dalec/frontend"
"github.com/moby/buildkit/client/llb"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
const (
outputDir = "/tmp/output"
buildScriptName = "_build.sh"
aptCachePrefix = "jammy-windowscross"
)
func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
sOpt, err := frontend.SourceOptFromClient(ctx, client, nil)
if err != nil {
return nil, nil, err
}
pg := dalec.ProgressGroup("Build windows container: " + spec.Name)
worker, err := distroConfig.Worker(sOpt, pg)
if err != nil {
return nil, nil, err
}
bin, err := buildBinaries(ctx, spec, worker, client, sOpt, targetKey, pg)
if err != nil {
return nil, nil, fmt.Errorf("unable to build binaries: %w", err)
}
st := getZipLLB(worker, platform, spec, bin, pg)
def, err := st.Marshal(ctx)
if err != nil {
return nil, nil, fmt.Errorf("error marshalling llb: %w", err)
}
res, err := client.Solve(ctx, gwclient.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, nil, err
}
ref, err := res.SingleRef()
return ref, &dalec.DockerImageSpec{}, err
})
}
const (
gomodsName = "__gomods"
cargohomeName = "__cargohome"
)
func specToSourcesLLB(worker llb.State, spec *dalec.Spec, sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (map[string]llb.State, error) {
out, err := dalec.Sources(spec, sOpt, opts...)
if err != nil {
return nil, errors.Wrap(err, "error preparign spec sources")
}
opts = append(opts, dalec.ProgressGroup("Add gomod sources"))
gomodSt, err := spec.GomodDeps(sOpt, worker, opts...)
if err != nil {
return nil, errors.Wrap(err, "error adding gomod sources")
}
cargohomeSt, err := spec.CargohomeDeps(sOpt, worker, opts...)
if err != nil {
return nil, errors.Wrap(err, "error adding cargohome sources")
}
if gomodSt != nil {
out[gomodsName] = *gomodSt
}
if cargohomeSt != nil {
out[cargohomeName] = *cargohomeSt
}
return out, nil
}
func withSourcesMounted(dst string, states map[string]llb.State, sources map[string]dalec.Source) llb.RunOption {
opts := make([]llb.RunOption, 0, len(states))
sorted := dalec.SortMapKeys(states)
files := []llb.State{}
for _, k := range sorted {
state := states[k]
// In cases where we have a generated source (e.g. gomods) we don't have a [dalec.Source] in the `sources` map.
// So we need to check for this.
src, ok := sources[k]
if ok && !dalec.SourceIsDir(src) {
files = append(files, state)
continue
}
dirDst := filepath.Join(dst, k)
opts = append(opts, llb.AddMount(dirDst, state))
}
ordered := make([]llb.RunOption, 1, len(opts)+1)
ordered[0] = llb.AddMount(dst, dalec.MergeAtPath(llb.Scratch(), files, "/"))
ordered = append(ordered, opts...)
return dalec.WithRunOptions(ordered...)
}
func buildBinaries(ctx context.Context, spec *dalec.Spec, worker llb.State, client gwclient.Client, sOpt dalec.SourceOpts, targetKey string, opts ...llb.ConstraintsOpt) (llb.State, error) {
worker = worker.With(distroConfig.InstallBuildDeps(sOpt, spec, targetKey, opts...))
sources, err := specToSourcesLLB(worker, spec, sOpt, opts...)
if err != nil {
return llb.Scratch(), errors.Wrap(err, "could not generate sources")
}
patched := dalec.PatchSources(worker, spec, sources, opts...)
buildScript := createBuildScript(spec, opts...)
artifacts := spec.GetArtifacts(targetKey)
script := generateInvocationScript(artifacts.Binaries)
builder := worker.With(dalec.SetBuildNetworkMode(spec))
st := builder.Run(
dalec.ShArgs(script.String()),
llb.Dir("/build"),
withSourcesMounted("/build", patched, spec.Sources),
llb.AddMount("/tmp/scripts", buildScript),
dalec.WithConstraints(opts...),
// We could check if we even need the var (ie there are gomods) but this
// is a fine default since we are expecting windows binaries. This
// means if someone eneds to build non-windows tooling as part of the
// build then they will need to set GOOS=linux manually.
// As such, this must come before the env vars from the spec are set.
llb.AddEnv("GOOS", "windows"),
dalec.RunOptFunc(func(ei *llb.ExecInfo) {
for k, v := range spec.Build.Env {
ei.State = ei.State.With(llb.AddEnv(k, v))
}
}),
).AddMount(outputDir, llb.Scratch())
return frontend.MaybeSign(ctx, client, st, spec, targetKey, sOpt)
}
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)
zipped := worker.Run(
dalec.ShArgs("zip "+outName+" *"),
llb.Dir("/tmp/artifacts"),
llb.AddMount("/tmp/artifacts", artifacts),
dalec.WithConstraints(opts...),
).AddMount(outputDir, llb.Scratch())
return zipped
}
func generateInvocationScript(binaries map[string]dalec.ArtifactConfig) *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)
if config.Permissions.Perm() != 0 {
fmt.Fprintf(script, "chmod %o '%s/%s'\n", config.Permissions.Perm(), outputDir, bin)
}
}
return script
}
func createBuildScript(spec *dalec.Spec, opts ...llb.ConstraintsOpt) llb.State {
buf := bytes.NewBuffer(nil)
fmt.Fprintln(buf, "#!/usr/bin/env sh")
fmt.Fprintln(buf, "set -x")
if spec.HasGomods() {
fmt.Fprintln(buf, "export GOMODCACHE=\"$(pwd)/"+gomodsName+"\"")
}
if spec.HasCargohomes() {
fmt.Fprintln(buf, "export CARGO_HOME=\"$(pwd)/"+cargohomeName+"\"")
}
for i, step := range spec.Build.Steps {
fmt.Fprintln(buf, "(")
for k, v := range step.Env {
fmt.Fprintf(buf, "export %s=\"%s\"", k, v)
}
fmt.Fprintln(buf, step.Command)
fmt.Fprintf(buf, ")")
if i < len(spec.Build.Steps)-1 {
fmt.Fprintln(buf, " && \\")
continue
}
fmt.Fprintf(buf, "\n")
}
return llb.Scratch().
File(llb.Mkfile(buildScriptName, 0o770, buf.Bytes()), opts...)
}