Skip to content

Commit ebea3ae

Browse files
authored
Merge pull request #2162 from buildpacks/build-with-platform
Support `pack build --platform`
2 parents 7306444 + 89cb617 commit ebea3ae

8 files changed

Lines changed: 285 additions & 117 deletions

File tree

acceptance/acceptance_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2699,6 +2699,21 @@ include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ]
26992699
})
27002700
})
27012701
})
2702+
2703+
when("--platform", func() {
2704+
it.Before(func() {
2705+
h.SkipIf(t, !pack.SupportsFeature(invoke.PlatformOption), "")
2706+
})
2707+
2708+
it("uses the builder with the desired platform", func() {
2709+
output, _ := pack.Run(
2710+
"build", repoName,
2711+
"-p", filepath.Join("testdata", "mock_app"),
2712+
"--platform", "linux/not-exist-arch",
2713+
)
2714+
h.AssertContainsMatch(t, output, "Pulling image '.*test/builder.*' with platform 'linux/not-exist-arch")
2715+
})
2716+
})
27022717
})
27032718

27042719
when("build --buildpack <flattened buildpack>", func() {

acceptance/invoke/pack.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ const (
239239
FlattenBuilderCreationV2
240240
FixesRunImageMetadata
241241
ManifestCommands
242+
PlatformOption
242243
MultiPlatformBuildersAndBuildPackages
243244
)
244245

@@ -279,6 +280,9 @@ var featureTests = map[Feature]func(i *PackInvoker) bool{
279280
ManifestCommands: func(i *PackInvoker) bool {
280281
return i.atLeast("v0.34.0")
281282
},
283+
PlatformOption: func(i *PackInvoker) bool {
284+
return i.atLeast("v0.34.0")
285+
},
282286
MultiPlatformBuildersAndBuildPackages: func(i *PackInvoker) bool {
283287
return i.atLeast("v0.34.0")
284288
},

internal/commands/build.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type BuildFlags struct {
3535
Builder string
3636
Registry string
3737
RunImage string
38+
Platform string
3839
Policy string
3940
Network string
4041
DescriptorPath string
@@ -132,6 +133,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
132133
if err != nil {
133134
return errors.Wrapf(err, "parsing pull policy %s", flags.Policy)
134135
}
136+
135137
var lifecycleImage string
136138
if flags.LifecycleImage != "" {
137139
ref, err := name.ParseReference(flags.LifecycleImage)
@@ -140,6 +142,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
140142
}
141143
lifecycleImage = ref.Name()
142144
}
145+
143146
var gid = -1
144147
if cmd.Flags().Changed("gid") {
145148
gid = flags.GID
@@ -165,6 +168,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
165168
Image: inputImageName.Name(),
166169
Publish: flags.Publish,
167170
DockerHost: flags.DockerHost,
171+
Platform: flags.Platform,
168172
PullPolicy: pullPolicy,
169173
ClearCache: flags.ClearCache,
170174
TrustBuilder: func(string) bool {
@@ -257,6 +261,7 @@ Special value 'inherit' may be used in which case DOCKER_HOST environment variab
257261
This option may set DOCKER_HOST environment variable for the build container if needed.
258262
`)
259263
cmd.Flags().StringVar(&buildFlags.LifecycleImage, "lifecycle-image", cfg.LifecycleImage, `Custom lifecycle image to use for analysis, restore, and export when builder is untrusted.`)
264+
cmd.Flags().StringVar(&buildFlags.Platform, "platform", "", `Platform to build on (e.g., "linux/amd64").`)
260265
cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`)
261266
cmd.Flags().StringVarP(&buildFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name")
262267
cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)")

internal/commands/build_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@ func testBuildCommand(t *testing.T, when spec.G, it spec.S) {
148148
})
149149
})
150150

151+
when("--platform", func() {
152+
it("sets platform", func() {
153+
mockClient.EXPECT().
154+
Build(gomock.Any(), EqBuildOptionsWithPlatform("linux/amd64")).
155+
Return(nil)
156+
157+
command.SetArgs([]string{"image", "--builder", "my-builder", "--platform", "linux/amd64"})
158+
h.AssertNil(t, command.Execute())
159+
})
160+
})
161+
151162
when("--pull-policy", func() {
152163
it("sets pull-policy=never", func() {
153164
mockClient.EXPECT().
@@ -958,6 +969,15 @@ func EqBuildOptionsDefaultProcess(defaultProc string) gomock.Matcher {
958969
}
959970
}
960971

972+
func EqBuildOptionsWithPlatform(platform string) gomock.Matcher {
973+
return buildOptionsMatcher{
974+
description: fmt.Sprintf("Platform=%s", platform),
975+
equals: func(o client.BuildOptions) bool {
976+
return o.Platform == platform
977+
},
978+
}
979+
}
980+
961981
func EqBuildOptionsWithPullPolicy(policy image.PullPolicy) gomock.Matcher {
962982
return buildOptionsMatcher{
963983
description: fmt.Sprintf("PullPolicy=%s", policy),

pkg/client/build.go

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ type BuildOptions struct {
159159
// Process type that will be used when setting container start command.
160160
DefaultProcessType string
161161

162+
// Platform is the desired platform to build on (e.g., linux/amd64)
163+
Platform string
164+
162165
// Strategy for updating local images before a build.
163166
PullPolicy image.PullPolicy
164167

@@ -320,32 +323,54 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
320323
return errors.Wrapf(err, "invalid builder '%s'", opts.Builder)
321324
}
322325

323-
rawBuilderImage, err := c.imageFetcher.Fetch(ctx, builderRef.Name(), image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy})
326+
requestedTarget := func() *dist.Target {
327+
if opts.Platform == "" {
328+
return nil
329+
}
330+
parts := strings.Split(opts.Platform, "/")
331+
switch len(parts) {
332+
case 0:
333+
return nil
334+
case 1:
335+
return &dist.Target{OS: parts[0]}
336+
case 2:
337+
return &dist.Target{OS: parts[0], Arch: parts[1]}
338+
default:
339+
return &dist.Target{OS: parts[0], Arch: parts[1], ArchVariant: parts[2]}
340+
}
341+
}()
342+
343+
rawBuilderImage, err := c.imageFetcher.Fetch(
344+
ctx,
345+
builderRef.Name(),
346+
image.FetchOptions{
347+
Daemon: true,
348+
Target: requestedTarget,
349+
PullPolicy: opts.PullPolicy},
350+
)
324351
if err != nil {
325352
return errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name())
326353
}
327354

328-
builderOS, err := rawBuilderImage.OS()
329-
if err != nil {
330-
return errors.Wrapf(err, "getting builder OS")
331-
}
332-
333-
builderArch, err := rawBuilderImage.Architecture()
334-
if err != nil {
335-
return errors.Wrapf(err, "getting builder architecture")
355+
var targetToUse *dist.Target
356+
if requestedTarget != nil {
357+
targetToUse = requestedTarget
358+
} else {
359+
targetToUse, err = getTargetFromBuilder(rawBuilderImage)
360+
if err != nil {
361+
return err
362+
}
336363
}
337364

338365
bldr, err := c.getBuilder(rawBuilderImage)
339366
if err != nil {
340367
return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder))
341368
}
342369

343-
target := &dist.Target{OS: builderOS, Arch: builderArch}
344-
345370
fetchOptions := image.FetchOptions{
346371
Daemon: !opts.Publish,
347372
PullPolicy: opts.PullPolicy,
348-
Target: target,
373+
Target: targetToUse,
349374
}
350375
runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, fetchOptions)
351376

@@ -374,12 +399,12 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
374399
return err
375400
}
376401

377-
fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Image(), bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts)
402+
fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts, targetToUse)
378403
if err != nil {
379404
return err
380405
}
381406

382-
fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Image(), bldr.Extensions(), bldr.OrderExtensions(), bldr.StackID, opts)
407+
fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Extensions(), opts, targetToUse)
383408
if err != nil {
384409
return err
385410
}
@@ -420,7 +445,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
420445
image.FetchOptions{
421446
Daemon: true,
422447
PullPolicy: opts.PullPolicy,
423-
Target: target,
448+
Target: targetToUse,
424449
},
425450
)
426451
if err != nil {
@@ -492,7 +517,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
492517
defer c.docker.ImageRemove(context.Background(), ephemeralBuilder.Name(), types.RemoveOptions{Force: true})
493518

494519
if len(bldr.OrderExtensions()) > 0 || len(ephemeralBuilder.OrderExtensions()) > 0 {
495-
if builderOS == "windows" {
520+
if targetToUse.OS == "windows" {
496521
return fmt.Errorf("builder contains image extensions which are not supported for Windows builds")
497522
}
498523
if !(opts.PullPolicy == image.PullAlways) {
@@ -504,7 +529,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
504529
opts.ContainerConfig.Volumes = appendLayoutVolumes(opts.ContainerConfig.Volumes, pathsConfig)
505530
}
506531

507-
processedVolumes, warnings, err := processVolumes(builderOS, opts.ContainerConfig.Volumes)
532+
processedVolumes, warnings, err := processVolumes(targetToUse.OS, opts.ContainerConfig.Volumes)
508533
if err != nil {
509534
return err
510535
}
@@ -735,6 +760,26 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
735760
return c.logImageNameAndSha(ctx, opts.Publish, imageRef)
736761
}
737762

763+
func getTargetFromBuilder(builderImage imgutil.Image) (*dist.Target, error) {
764+
builderOS, err := builderImage.OS()
765+
if err != nil {
766+
return nil, fmt.Errorf("failed to get builder OS: %w", err)
767+
}
768+
builderArch, err := builderImage.Architecture()
769+
if err != nil {
770+
return nil, fmt.Errorf("failed to get builder architecture: %w", err)
771+
}
772+
builderArchVariant, err := builderImage.Variant()
773+
if err != nil {
774+
return nil, fmt.Errorf("failed to get builder architecture variant: %w", err)
775+
}
776+
return &dist.Target{
777+
OS: builderOS,
778+
Arch: builderArch,
779+
ArchVariant: builderArchVariant,
780+
}, nil
781+
}
782+
738783
func extractSupportedLifecycleApis(labels map[string]string) ([]string, error) {
739784
// sample contents of labels:
740785
// {io.buildpacks.builder.metadata:\"{\"lifecycle\":{\"version\":\"0.15.3\"},\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}}",
@@ -1087,7 +1132,7 @@ func (c *Client) processProxyConfig(config *ProxyConfig) ProxyConfig {
10871132
// ----------
10881133
// - group:
10891134
// - A
1090-
func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Image, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions) (fetchedBPs []buildpack.BuildModule, order dist.Order, err error) {
1135+
func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions, targetToUse *dist.Target) (fetchedBPs []buildpack.BuildModule, order dist.Order, err error) {
10911136
relativeBaseDir := opts.RelativeBaseDir
10921137
declaredBPs := opts.Buildpacks
10931138

@@ -1130,7 +1175,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima
11301175
order = newOrder
11311176
}
11321177
default:
1133-
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack)
1178+
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
11341179
if err != nil {
11351180
return fetchedBPs, order, err
11361181
}
@@ -1164,7 +1209,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima
11641209
if len(preBuildpacks) > 0 || len(postBuildpacks) > 0 {
11651210
order = builderOrder
11661211
for _, bp := range preBuildpacks {
1167-
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack)
1212+
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
11681213
if err != nil {
11691214
return fetchedBPs, order, err
11701215
}
@@ -1173,7 +1218,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima
11731218
}
11741219

11751220
for _, bp := range postBuildpacks {
1176-
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack)
1221+
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
11771222
if err != nil {
11781223
return fetchedBPs, order, err
11791224
}
@@ -1186,7 +1231,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima
11861231
return fetchedBPs, order, nil
11871232
}
11881233

1189-
func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir string, builderImage imgutil.Image, builderBPs []dist.ModuleInfo, opts BuildOptions, kind string) ([]buildpack.BuildModule, *dist.ModuleInfo, error) {
1234+
func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir string, builderBPs []dist.ModuleInfo, opts BuildOptions, kind string, targetToUse *dist.Target) ([]buildpack.BuildModule, *dist.ModuleInfo, error) {
11901235
pullPolicy := opts.PullPolicy
11911236
publish := opts.Publish
11921237
registry := opts.Registry
@@ -1206,19 +1251,9 @@ func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir
12061251
Version: version,
12071252
}
12081253
default:
1209-
builderOS, err := builderImage.OS()
1210-
if err != nil {
1211-
return nil, nil, errors.Wrapf(err, "getting builder OS")
1212-
}
1213-
1214-
builderArch, err := builderImage.Architecture()
1215-
if err != nil {
1216-
return nil, nil, errors.Wrapf(err, "getting builder architecture")
1217-
}
1218-
12191254
downloadOptions := buildpack.DownloadOptions{
12201255
RegistryName: registry,
1221-
Target: &dist.Target{OS: builderOS, Arch: builderArch},
1256+
Target: targetToUse,
12221257
RelativeBaseDir: relativeBaseDir,
12231258
Daemon: !publish,
12241259
PullPolicy: pullPolicy,
@@ -1322,7 +1357,7 @@ func prependBuildpackToOrder(order dist.Order, bpInfo dist.ModuleInfo) (newOrder
13221357
return newOrder
13231358
}
13241359

1325-
func (c *Client) processExtensions(ctx context.Context, builderImage imgutil.Image, builderExs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) {
1360+
func (c *Client) processExtensions(ctx context.Context, builderExs []dist.ModuleInfo, opts BuildOptions, targetToUse *dist.Target) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) {
13261361
relativeBaseDir := opts.RelativeBaseDir
13271362
declaredExs := opts.Extensions
13281363

@@ -1339,7 +1374,7 @@ func (c *Client) processExtensions(ctx context.Context, builderImage imgutil.Ima
13391374
case buildpack.FromBuilderLocator:
13401375
return nil, nil, errors.New("from builder is not supported for extensions")
13411376
default:
1342-
newFetchedExs, moduleInfo, err := c.fetchBuildpack(ctx, ex, relativeBaseDir, builderImage, builderExs, opts, buildpack.KindExtension)
1377+
newFetchedExs, moduleInfo, err := c.fetchBuildpack(ctx, ex, relativeBaseDir, builderExs, opts, buildpack.KindExtension, targetToUse)
13431378
if err != nil {
13441379
return fetchedExs, orderExtensions, err
13451380
}

0 commit comments

Comments
 (0)