Skip to content

Commit 6e25b3d

Browse files
authored
Allow merging identical source files from different pallets to the same target (#307)
* Allow merging identical source files from different pallets to the same target * Bump changelog version for v0.8.0-alpha.2 prerelease * Use two-space indent for generating bundle manifest files * Show info about overridden required pallets in bundle manifest files * Report file imports & provenance of imported files in bundle manifests * Attach version strings to overriding pallets/repos without dirty changes, when inferrable via git * Show info about transitive pallet requirements in bundle manifest * Bump changelog version for v0.8.0-alpha.2 prerelease
1 parent 245b912 commit 6e25b3d

File tree

7 files changed

+335
-98
lines changed

7 files changed

+335
-98
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## Unreleased
8+
## 0.8.0-alpha.2 - 2024-09-22
99

1010
### Added
1111

@@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- (cli) Added a `[dev] plt locate-plt-file` command to print the actual filesystem path of the specified file in the specified pallet required by the local/development pallet.
1919
- (cli) Added a `[dev] plt show-plt-file` command to print the contents of the specified file in the specified pallet required by the local/development pallet.
2020
- (cli) Added a `cache rm-dl` command to delete the cache of downloaded files for export.
21+
- (cli) The bundle manifest's `includes` section's description of required pallets now reports when required pallets were overridden.
22+
- (cli) The bundle manifest's `includes` section's description of required pallets now recursively shows information about transitively-required pallets (but does not show information about file import groups in those transitively-required pallets).
23+
- (cli) The bundle manifest's `includes` section's description of required pallets now shows the results (as target file path -> source file path mappings) of evaluating each file import group attached to their respective required pallets.
24+
- (cli) The bundle manifest now has an `imports` section which describes the provenance of each imported file, as a list of how the file has been transitively imported across pallets (with pallets farther down the list being depeer in the transitive import chain).
2125

2226
### Changed
2327

cmd/forklift/dev/plt/pallets.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ func loadReplacementPallets(fsPaths []string) (replacements []*forklift.FSPallet
133133
if len(externalPallets) == 0 {
134134
return nil, errors.Errorf("no replacement pallets found in path %s", replacementPath)
135135
}
136+
for _, pallet := range externalPallets {
137+
version, clean := fcli.CheckGitRepoVersion(pallet.FS.Path())
138+
if clean {
139+
pallet.Version = version
140+
}
141+
}
136142
replacements = append(replacements, externalPallets...)
137143
}
138144
return replacements, nil
@@ -203,6 +209,12 @@ func loadReplacementRepos(
203209
if len(externalRepos) == 0 {
204210
return nil, errors.Errorf("no replacement repos found in path %s", replacementPath)
205211
}
212+
for _, repo := range externalRepos {
213+
version, clean := fcli.CheckGitRepoVersion(repo.FS.Path())
214+
if clean {
215+
repo.Version = version
216+
}
217+
}
206218
replacements = append(replacements, externalRepos...)
207219
}
208220
return replacements, nil

cmd/forklift/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const (
101101
bundleMinVersion = "v0.7.0"
102102
// newBundleVersion is the Forklift version reported in new staged pallet bundles made by Forklift.
103103
// Older versions of the Forklift tool cannot use such bundles.
104-
newBundleVersion = "v0.7.0"
104+
newBundleVersion = "v0.8.0-alpha.2"
105105
// newStageStoreVersion is the Forklift version reported in a stage store initialized by Forklift.
106106
// Older versions of the Forklift tool cannot use the state store.
107107
newStageStoreVersion = "v0.7.0"

internal/app/forklift/bundles-models.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,16 @@ type BundleManifest struct {
5151
Pallet BundlePallet `yaml:"pallet"`
5252
// Includes describes repos and pallets used to define the bundle's package deployments.
5353
Includes BundleInclusions `yaml:"includes,omitempty"`
54-
// Deploys describes deployments provided by the bundle. Keys are names of deployments.
55-
Deploys map[string]DeplDef `yaml:"deploys,omitempty"`
54+
// Imports lists the files imported from required pallets and the fully-qualified paths of those
55+
// source files (relative to their respective source pallets). Keys are the target paths of the
56+
// files, while values are lists showing the chain of provenance of the respective files (with
57+
// the deepest ancestor at the end of each list).
58+
Imports map[string][]string `yaml:"imports,omitempty"`
5659
// Downloads lists the URLs of files and OCI images downloaded for export by the bundle's
5760
// deployments. Keys are names of the bundle's deployments which export downloaded files.
5861
Downloads map[string][]string `yaml:"downloads,omitempty"`
62+
// Deploys describes deployments provided by the bundle. Keys are names of deployments.
63+
Deploys map[string]DeplDef `yaml:"deploys,omitempty"`
5964
// Exports lists the target paths of file exports provided by the bundle's deployments. Keys are
6065
// names of the bundle's deployments which provide file exports.
6166
Exports map[string][]string `yaml:"exports,omitempty"`
@@ -90,6 +95,13 @@ type BundlePalletInclusion struct {
9095
// Override describes the pallet used to override the required pallet, if an override was
9196
// specified for the pallet when building the bundled pallet.
9297
Override BundleInclusionOverride `yaml:"override,omitempty"`
98+
// Includes describes pallets used to define the pallet, omitting information about file imports.
99+
Includes map[string]BundlePalletInclusion `yaml:"includes,omitempty"`
100+
// Imports lists the files imported from the pallet, organized by import group. Keys are the names
101+
// of the import groups, and values are the results of evaluating the respective import groups -
102+
// i.e. maps whose keys are target file paths (where the files are imported to) and whose values
103+
// are source file paths (where the files are imported from).
104+
Imports map[string]map[string]string `yaml:"imports,omitempty"`
93105
}
94106

95107
// BundleRepoInclusion describes a package repository used to build the bundled pallet.

internal/app/forklift/bundles.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package forklift
22

33
import (
44
"archive/tar"
5+
"bytes"
56
"compress/gzip"
67
"fmt"
78
"io"
@@ -60,13 +61,16 @@ func LoadFSBundle(fsys core.PathedFS, subdirPath string) (b *FSBundle, err error
6061
}
6162

6263
func (b *FSBundle) WriteManifestFile() error {
63-
marshaled, err := yaml.Marshal(b.Manifest)
64-
if err != nil {
64+
buf := bytes.Buffer{}
65+
encoder := yaml.NewEncoder(&buf)
66+
const yamlIndent = 2
67+
encoder.SetIndent(yamlIndent)
68+
if err := encoder.Encode(b.Manifest); err != nil {
6569
return errors.Wrapf(err, "couldn't marshal bundle manifest")
6670
}
6771
outputPath := filepath.FromSlash(path.Join(b.FS.Path(), BundleManifestFile))
6872
const perm = 0o644 // owner rw, group r, public r
69-
if err := os.WriteFile(outputPath, marshaled, perm); err != nil {
73+
if err := os.WriteFile(outputPath, buf.Bytes(), perm); err != nil {
7074
return errors.Wrapf(err, "couldn't save bundle manifest to %s", outputPath)
7175
}
7276
return nil

internal/app/forklift/cli/staging.go

Lines changed: 147 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"path"
88
"path/filepath"
9+
"strings"
910

1011
dct "github.com/compose-spec/compose-go/v2/types"
1112
"github.com/pkg/errors"
@@ -73,24 +74,24 @@ type StagingCaches struct {
7374
}
7475

7576
func StagePallet(
76-
indent int, pallet *forklift.FSPallet, stageStore *forklift.FSStageStore, caches StagingCaches,
77+
indent int, merged *forklift.FSPallet, stageStore *forklift.FSStageStore, caches StagingCaches,
7778
exportPath string, versions StagingVersions,
7879
skipImageCaching, parallel, ignoreToolVersion bool,
7980
) (index int, err error) {
80-
if _, isMerged := pallet.FS.(*forklift.MergeFS); isMerged {
81+
if _, isMerged := merged.FS.(*forklift.MergeFS); isMerged {
8182
return 0, errors.Errorf("the pallet provided for staging should not be a merged pallet!")
8283
}
8384

84-
pallet, repoCacheWithMerged, err := CacheStagingReqs(
85-
0, pallet, caches.Mirrors, caches.Pallets, caches.Repos, caches.Downloads, false, parallel,
85+
merged, repoCacheWithMerged, err := CacheStagingReqs(
86+
0, merged, caches.Mirrors, caches.Pallets, caches.Repos, caches.Downloads, false, parallel,
8687
)
8788
if err != nil {
8889
return 0, errors.Wrap(err, "couldn't cache requirements for staging the pallet")
8990
}
9091
// Note: we must have all requirements in the cache before we can check their compatibility with
9192
// the Forklift tool version
9293
if err = CheckDeepCompat(
93-
pallet, caches.Pallets, repoCacheWithMerged, versions.Core, ignoreToolVersion,
94+
merged, caches.Pallets, repoCacheWithMerged, versions.Core, ignoreToolVersion,
9495
); err != nil {
9596
return 0, err
9697
}
@@ -102,10 +103,10 @@ func StagePallet(
102103
}
103104
fmt.Printf("Bundling pallet as stage %d for staged application...\n", index)
104105
if err = buildBundle(
105-
pallet, repoCacheWithMerged, caches.Downloads,
106+
merged, caches.Pallets, repoCacheWithMerged, caches.Downloads,
106107
versions.NewBundle, path.Join(stageStore.FS.Path(), fmt.Sprintf("%d", index)),
107108
); err != nil {
108-
return index, errors.Wrapf(err, "couldn't bundle pallet %s as stage %d", pallet.Path(), index)
109+
return index, errors.Wrapf(err, "couldn't bundle pallet %s as stage %d", merged.Path(), index)
109110
}
110111
if err = SetNextStagedBundle(
111112
indent, stageStore, index, exportPath, versions.Core.Tool, versions.MinSupportedBundle,
@@ -119,19 +120,18 @@ func StagePallet(
119120
}
120121

121122
func buildBundle(
122-
pallet *forklift.FSPallet,
123-
repoCache forklift.PathedRepoCache, dlCache *forklift.FSDownloadCache,
123+
merged *forklift.FSPallet,
124+
palletCache forklift.PathedPalletCache, repoCache forklift.PathedRepoCache,
125+
dlCache *forklift.FSDownloadCache,
124126
forkliftVersion, outputPath string,
125127
) (err error) {
126128
outputBundle := forklift.NewFSBundle(outputPath)
127-
// TODO: once we can overlay pallets, save the result of overlaying the pallets to a `overlay`
128-
// subdir
129-
outputBundle.Manifest, err = newBundleManifest(pallet, repoCache, forkliftVersion)
129+
outputBundle.Manifest, err = newBundleManifest(merged, palletCache, repoCache, forkliftVersion)
130130
if err != nil {
131131
return errors.Wrapf(err, "couldn't create bundle manifest for %s", outputBundle.FS.Path())
132132
}
133133

134-
depls, _, err := Check(0, pallet, repoCache)
134+
depls, _, err := Check(0, merged, repoCache)
135135
if err != nil {
136136
return errors.Wrap(err, "couldn't ensure pallet validity")
137137
}
@@ -141,8 +141,8 @@ func buildBundle(
141141
}
142142
}
143143

144-
if err := outputBundle.SetBundledPallet(pallet); err != nil {
145-
return errors.Wrapf(err, "couldn't write pallet %s into bundle", pallet.Def.Pallet.Path)
144+
if err := outputBundle.SetBundledPallet(merged); err != nil {
145+
return errors.Wrapf(err, "couldn't write pallet %s into bundle", merged.Def.Pallet.Path)
146146
}
147147
if err = outputBundle.WriteRepoDefFile(); err != nil {
148148
return errors.Wrap(err, "couldn't write repo declaration into bundle")
@@ -157,13 +157,15 @@ func buildBundle(
157157
}
158158

159159
func newBundleManifest(
160-
pallet *forklift.FSPallet, repoCache forklift.PathedRepoCache, forkliftVersion string,
160+
merged *forklift.FSPallet,
161+
palletCache forklift.PathedPalletCache, repoCache forklift.PathedRepoCache,
162+
forkliftVersion string,
161163
) (forklift.BundleManifest, error) {
162164
desc := forklift.BundleManifest{
163165
ForkliftVersion: forkliftVersion,
164166
Pallet: forklift.BundlePallet{
165-
Path: pallet.Path(),
166-
Description: pallet.Def.Pallet.Description,
167+
Path: merged.Path(),
168+
Description: merged.Def.Pallet.Description,
167169
},
168170
Includes: forklift.BundleInclusions{
169171
Pallets: make(map[string]forklift.BundlePalletInclusion),
@@ -173,30 +175,45 @@ func newBundleManifest(
173175
Downloads: make(map[string][]string),
174176
Exports: make(map[string][]string),
175177
}
176-
desc.Pallet.Version, desc.Pallet.Clean = checkGitRepoVersion(pallet.FS.Path())
177-
palletReqs, err := pallet.LoadFSPalletReqs("**")
178+
desc.Pallet.Version, desc.Pallet.Clean = CheckGitRepoVersion(merged.FS.Path())
179+
palletReqs, err := merged.LoadFSPalletReqs("**")
178180
if err != nil {
179-
return desc, errors.Wrapf(err, "couldn't determine pallets required by pallet %s", pallet.Path())
181+
return desc, errors.Wrapf(err, "couldn't determine pallets required by pallet %s", merged.Path())
180182
}
181-
// TODO: once we can overlay pallets, the description of pallet & repo inclusions should probably
182-
// be made from the result of overlaying. We could also describe pre-overlay requirements from the
183-
// bundled pallet, in desc.Pallet.Requires.
184183
for _, req := range palletReqs {
185-
inclusion := forklift.BundlePalletInclusion{Req: req.PalletReq}
186-
// TODO: also check for overridden pallets
187-
desc.Includes.Pallets[req.RequiredPath] = inclusion
184+
if desc.Includes.Pallets[req.RequiredPath], err = newBundlePalletInclusion(
185+
merged, req, palletCache, true,
186+
); err != nil {
187+
return desc, errors.Wrapf(
188+
err, "couldn't generate description of requirement for pallet %s", req.RequiredPath,
189+
)
190+
}
188191
}
189-
repoReqs, err := pallet.LoadFSRepoReqs("**")
192+
repoReqs, err := merged.LoadFSRepoReqs("**")
190193
if err != nil {
191-
return desc, errors.Wrapf(err, "couldn't determine repos required by pallet %s", pallet.Path())
194+
return desc, errors.Wrapf(err, "couldn't determine repos required by pallet %s", merged.Path())
192195
}
193196
for _, req := range repoReqs {
194197
desc.Includes.Repos[req.RequiredPath] = newBundleRepoInclusion(req, repoCache)
195198
}
199+
if mergeFS, ok := merged.FS.(*forklift.MergeFS); ok {
200+
imports, err := mergeFS.ListImports()
201+
if err != nil {
202+
return desc, errors.Wrapf(err, "couldn't list pallet file import groups")
203+
}
204+
desc.Imports = make(map[string][]string)
205+
for target, sourceRef := range imports {
206+
sources := make([]string, 0, len(sourceRef.Sources))
207+
for _, source := range sourceRef.Sources {
208+
sources = append(sources, path.Join(source, sourceRef.Path))
209+
}
210+
desc.Imports[target] = sources
211+
}
212+
}
196213
return desc, nil
197214
}
198215

199-
func checkGitRepoVersion(palletPath string) (version string, clean bool) {
216+
func CheckGitRepoVersion(palletPath string) (version string, clean bool) {
200217
gitRepo, err := git.Open(filepath.FromSlash(palletPath))
201218
if err != nil {
202219
return "", false
@@ -220,6 +237,102 @@ func checkGitRepoVersion(palletPath string) (version string, clean bool) {
220237
return versionString, status.IsClean()
221238
}
222239

240+
func newBundlePalletInclusion(
241+
pallet *forklift.FSPallet, req *forklift.FSPalletReq, palletCache forklift.PathedPalletCache,
242+
describeImports bool,
243+
) (inclusion forklift.BundlePalletInclusion, err error) {
244+
inclusion = forklift.BundlePalletInclusion{
245+
Req: req.PalletReq,
246+
Includes: make(map[string]forklift.BundlePalletInclusion),
247+
}
248+
for {
249+
if palletCache == nil {
250+
break
251+
}
252+
layeredCache, ok := palletCache.(*forklift.LayeredPalletCache)
253+
if !ok {
254+
break
255+
}
256+
overlay := layeredCache.Overlay
257+
if overlay == nil {
258+
palletCache = layeredCache.Underlay
259+
continue
260+
}
261+
262+
if loaded, err := overlay.LoadFSPallet(req.RequiredPath, req.VersionLock.Version); err == nil {
263+
// i.e. the pallet was overridden
264+
inclusion.Override.Path = loaded.FS.Path()
265+
inclusion.Override.Version, inclusion.Override.Clean = CheckGitRepoVersion(loaded.FS.Path())
266+
break
267+
}
268+
palletCache = layeredCache.Underlay
269+
}
270+
271+
loaded, err := palletCache.LoadFSPallet(req.RequiredPath, req.VersionLock.Version)
272+
if err != nil {
273+
return inclusion, errors.Wrapf(err, "couldn't load pallet %s", req.RequiredPath)
274+
}
275+
palletReqs, err := loaded.LoadFSPalletReqs("**")
276+
if err != nil {
277+
return inclusion, errors.Wrapf(
278+
err, "couldn't determine pallets required by pallet %s", loaded.Path(),
279+
)
280+
}
281+
for _, req := range palletReqs {
282+
if inclusion.Includes[req.RequiredPath], err = newBundlePalletInclusion(
283+
loaded, req, palletCache, false,
284+
); err != nil {
285+
return inclusion, errors.Wrapf(
286+
err, "couldn't generate description of transitive requirement for pallet %s", loaded.Path(),
287+
)
288+
}
289+
}
290+
291+
if !describeImports {
292+
return inclusion, nil
293+
}
294+
if inclusion.Imports, err = describePalletImports(pallet, req, palletCache); err != nil {
295+
return inclusion, errors.Wrapf(err, "couldn't describe file imports for %s", req.RequiredPath)
296+
}
297+
return inclusion, nil
298+
}
299+
300+
func describePalletImports(
301+
pallet *forklift.FSPallet, req *forklift.FSPalletReq, palletCache forklift.PathedPalletCache,
302+
) (fileMappings map[string]map[string]string, err error) {
303+
imports, err := pallet.LoadImports(path.Join(req.RequiredPath, "**/*"))
304+
if err != nil {
305+
return nil, errors.Wrap(err, "couldn't load file import groups")
306+
}
307+
allResolved, err := forklift.ResolveImports(pallet, palletCache, imports)
308+
if err != nil {
309+
return nil, errors.Wrap(err, "couldn't resolve file import groups")
310+
}
311+
requiredPallets := make(map[string]*forklift.FSPallet) // pallet path -> pallet
312+
for _, resolved := range allResolved {
313+
requiredPallets[resolved.Pallet.Path()] = resolved.Pallet
314+
}
315+
for palletPath, requiredPallet := range requiredPallets {
316+
if requiredPallets[palletPath], err = forklift.MergeFSPallet(
317+
requiredPallet, palletCache, nil,
318+
); err != nil {
319+
return nil, errors.Wrapf(
320+
err, "couldn't compute merged pallet for required pallet %s", palletPath,
321+
)
322+
}
323+
}
324+
325+
fileMappings = make(map[string]map[string]string)
326+
for _, resolved := range allResolved {
327+
resolved.Pallet = requiredPallets[req.RequiredPath]
328+
importName := strings.TrimPrefix(resolved.Name, req.RequiredPath+"/")
329+
if fileMappings[importName], err = resolved.Evaluate(palletCache); err != nil {
330+
return nil, errors.Wrapf(err, "couldn't evaluate file import group %s", importName)
331+
}
332+
}
333+
return fileMappings, nil
334+
}
335+
223336
func newBundleRepoInclusion(
224337
req *forklift.FSRepoReq, repoCache forklift.PathedRepoCache,
225338
) forklift.BundleRepoInclusion {
@@ -238,11 +351,10 @@ func newBundleRepoInclusion(
238351
continue
239352
}
240353

241-
if repo, err := overlay.LoadFSRepo(
242-
req.RequiredPath, req.VersionLock.Version,
243-
); err == nil { // i.e. the repo was overridden
244-
inclusion.Override.Path = repo.FS.Path()
245-
inclusion.Override.Version, inclusion.Override.Clean = checkGitRepoVersion(repo.FS.Path())
354+
if loaded, err := overlay.LoadFSRepo(req.RequiredPath, req.VersionLock.Version); err == nil {
355+
// i.e. the repo was overridden
356+
inclusion.Override.Path = loaded.FS.Path()
357+
inclusion.Override.Version, inclusion.Override.Clean = CheckGitRepoVersion(loaded.FS.Path())
246358
return inclusion
247359
}
248360
repoCache = layeredCache.Underlay

0 commit comments

Comments
 (0)