Skip to content

Commit 59f0b62

Browse files
Ryan Moransophiewigmore
authored andcommitted
Implements split build/launch layers
1 parent 2a90ee5 commit 59f0b62

25 files changed

Lines changed: 935 additions & 414 deletions

build.go

Lines changed: 144 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,78 +7,183 @@ import (
77

88
"github.com/paketo-buildpacks/packit"
99
"github.com/paketo-buildpacks/packit/chronos"
10+
"github.com/paketo-buildpacks/packit/fs"
1011
)
1112

1213
//go:generate faux --interface InstallProcess --output fakes/install_process.go
14+
//go:generate faux --interface EntryResolver --output fakes/entry_resolver.go
15+
16+
// InstallProcess defines the interface for executing the "bundle install"
17+
// build process.
1318
type InstallProcess interface {
14-
ShouldRun(layer packit.Layer, workingDir string) (should bool, checksum string, rubyVersion string, err error)
19+
ShouldRun(metadata map[string]interface{}, workingDir string) (should bool, checksum string, rubyVersion string, err error)
1520
Execute(workingDir, layerPath string, config map[string]string) error
1621
}
1722

18-
//go:generate faux --interface EntryResolver --output fakes/entry_resolver.go
23+
// EntryResolver defines the interface for determining what phases of the
24+
// lifecycle will require gems.
1925
type EntryResolver interface {
2026
MergeLayerTypes(string, []packit.BuildpackPlanEntry) (launch, build bool)
2127
}
2228

29+
// Build will return a packit.BuildFunc that will be invoked during the build
30+
// phase of the buildpack lifecycle.
31+
//
32+
// Build will execute the installation process to install gems that can be used
33+
// in the build or launch phases of the buildpack lifecycle. Specifically,
34+
// Build will provide different sets of gems as discrete layers depending upon
35+
// their requirement in either the build or launch phase.
36+
//
37+
// If gems are required during the build phase, Build will ensure that all
38+
// gems, including those in the "development" and "test" groups are installed
39+
// into a layer that is made available during the remainder of the build phase.
40+
//
41+
// If gems are required during the launch phase, Build will ensure that only
42+
// those gems that are not in the "development" or "test" groups are installed
43+
// into a layer that is made available during the launch phase.
44+
//
45+
// If gems are required during both the build and launch phases, Build will
46+
// provide both of the above layers with their sets of gems. These layers
47+
// operate mutually exclusively as only one is available in each of the build
48+
// or launch phase.
49+
//
50+
// To improve performance when installing gems for use in both the build and
51+
// launch phases, Build will copy the contents of the build layer into the
52+
// launch layer before executing the launch layer installation process. This
53+
// will result in the launch layer installation process performing an effective
54+
// "no-op" as all of the gems that it requires should already be copied into
55+
// the layer. The launch layer installation process will however perform a
56+
// "bundle clean" to remove any extra gems, including those from the
57+
// "development" and "test" groups that may have been copied from the build
58+
// layer.
59+
//
60+
// Finally, upon completing the installation process, Build will remove any
61+
// local bundler configuration files such that the Bundler CLI will only follow
62+
// configuration from the global location, which will be configured to point to
63+
// a file that is maintained in each of the build and launch layers
64+
// respectively.
2365
func Build(installProcess InstallProcess, logger LogEmitter, clock chronos.Clock, entries EntryResolver) packit.BuildFunc {
2466
return func(context packit.BuildContext) (packit.BuildResult, error) {
2567
logger.Title("%s %s", context.BuildpackInfo.Name, context.BuildpackInfo.Version)
2668

27-
gemsLayer, err := context.Layers.Get(LayerNameGems)
28-
if err != nil {
29-
return packit.BuildResult{}, err
30-
}
69+
launch, build := entries.MergeLayerTypes("gems", context.Plan.Entries)
3170

32-
should, sum, rubyVersion, err := installProcess.ShouldRun(gemsLayer, context.WorkingDir)
33-
if err != nil {
34-
return packit.BuildResult{}, err
35-
}
71+
var layers []packit.Layer
72+
73+
if build {
74+
layer, err := context.Layers.Get(LayerNameBuildGems)
75+
if err != nil {
76+
return packit.BuildResult{}, err
77+
}
3678

37-
if !should {
38-
logger.Process("Reusing cached layer %s", gemsLayer.Path)
39-
logger.Break()
79+
layer.Build = true
80+
layer.Cache = true
4081

41-
err := os.RemoveAll(filepath.Join(context.WorkingDir, ".bundle", "config"))
82+
should, checksum, rubyVersion, err := installProcess.ShouldRun(layer.Metadata, context.WorkingDir)
4283
if err != nil {
4384
return packit.BuildResult{}, err
4485
}
4586

46-
return packit.BuildResult{Layers: []packit.Layer{gemsLayer}}, nil
87+
if should {
88+
logger.Process("Executing build environment install process")
89+
90+
duration, err := clock.Measure(func() error {
91+
return installProcess.Execute(context.WorkingDir, layer.Path, map[string]string{
92+
"path": layer.Path,
93+
"clean": "true",
94+
})
95+
})
96+
if err != nil {
97+
return packit.BuildResult{}, err
98+
}
99+
100+
logger.Action("Completed in %s", duration.Round(time.Millisecond))
101+
logger.Break()
102+
103+
layer.BuildEnv.Default("BUNDLE_USER_CONFIG", filepath.Join(layer.Path, "config"))
104+
layer.Metadata = map[string]interface{}{
105+
"built_at": clock.Now().Format(time.RFC3339Nano),
106+
"cache_sha": checksum,
107+
"ruby_version": rubyVersion,
108+
}
109+
} else {
110+
logger.Process("Reusing cached layer %s", layer.Path)
111+
logger.Break()
112+
}
113+
114+
layers = append(layers, layer)
47115
}
48116

49-
logger.Process("Executing build process")
117+
if launch {
118+
layer, err := context.Layers.Get(LayerNameLaunchGems)
119+
if err != nil {
120+
return packit.BuildResult{}, err
121+
}
50122

51-
duration, err := clock.Measure(func() error {
52-
err := installProcess.Execute(context.WorkingDir, gemsLayer.Path, map[string]string{
53-
"path": gemsLayer.Path,
54-
"without": "development:test",
55-
"clean": "true",
56-
})
123+
layer.Launch = true
124+
125+
should, checksum, rubyVersion, err := installProcess.ShouldRun(layer.Metadata, context.WorkingDir)
57126
if err != nil {
58-
return err
127+
return packit.BuildResult{}, err
59128
}
60129

61-
return os.RemoveAll(filepath.Join(context.WorkingDir, ".bundle", "config"))
62-
})
63-
if err != nil {
64-
return packit.BuildResult{}, err
65-
}
130+
if should {
131+
logger.Process("Executing launch environment install process")
132+
133+
duration, err := clock.Measure(func() error {
134+
if build {
135+
buildLayer, err := context.Layers.Get(LayerNameBuildGems)
136+
if err != nil {
137+
return err
138+
}
139+
140+
err = fs.Copy(filepath.Join(buildLayer.Path), filepath.Join(layer.Path))
141+
if err != nil {
142+
return err
143+
}
144+
}
145+
146+
return installProcess.Execute(context.WorkingDir, layer.Path, map[string]string{
147+
"path": layer.Path,
148+
"without": "development:test",
149+
"clean": "true",
150+
})
151+
})
152+
if err != nil {
153+
return packit.BuildResult{}, err
154+
}
155+
156+
logger.Action("Completed in %s", duration.Round(time.Millisecond))
157+
logger.Break()
158+
159+
layer.LaunchEnv.Default("BUNDLE_USER_CONFIG", filepath.Join(layer.Path, "config"))
160+
layer.Metadata = map[string]interface{}{
161+
"built_at": clock.Now().Format(time.RFC3339Nano),
162+
"cache_sha": checksum,
163+
"ruby_version": rubyVersion,
164+
}
165+
} else {
166+
logger.Process("Reusing cached layer %s", layer.Path)
167+
logger.Break()
168+
}
66169

67-
logger.Action("Completed in %s", duration.Round(time.Millisecond))
68-
logger.Break()
170+
layers = append(layers, layer)
171+
}
69172

70-
gemsLayer.Launch, gemsLayer.Build = entries.MergeLayerTypes("gems", context.Plan.Entries)
71-
gemsLayer.Cache = gemsLayer.Build
173+
for _, layer := range layers {
174+
logger.Environment(layer)
175+
}
72176

73-
gemsLayer.Metadata = map[string]interface{}{
74-
"built_at": clock.Now().Format(time.RFC3339Nano),
75-
"cache_sha": sum,
76-
"ruby_version": rubyVersion,
177+
err := os.RemoveAll(filepath.Join(context.WorkingDir, ".bundle", "config"))
178+
if err != nil {
179+
return packit.BuildResult{}, err
77180
}
78181

79-
gemsLayer.SharedEnv.Default("BUNDLE_USER_CONFIG", filepath.Join(gemsLayer.Path, "config"))
80-
logger.Environment(gemsLayer.SharedEnv)
182+
err = os.RemoveAll(filepath.Join(context.WorkingDir, ".bundle", "config.bak"))
183+
if err != nil {
184+
return packit.BuildResult{}, err
185+
}
81186

82-
return packit.BuildResult{Layers: []packit.Layer{gemsLayer}}, nil
187+
return packit.BuildResult{Layers: layers}, nil
83188
}
84189
}

0 commit comments

Comments
 (0)