diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd8101d81..8579493b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Add `buf registry plugin {create,delete,info,update}` commands to manage BSR plugins. - Breaking analysis support for `buf beta lsp`. +- Fix bug when using the `--type` flag filter for `buf build` where import ordering is not + determinisitic. ## [v1.47.2] - 2024-11-14 diff --git a/private/bufpkg/bufimage/bufimageutil/bufimageutil.go b/private/bufpkg/bufimage/bufimageutil/bufimageutil.go index 414dbb7ef2..c37338791e 100644 --- a/private/bufpkg/bufimage/bufimageutil/bufimageutil.go +++ b/private/bufpkg/bufimage/bufimageutil/bufimageutil.go @@ -266,16 +266,20 @@ func ImageFilteredByTypesWithOptions(image bufimage.Image, types []string, opts // the file's WeakDependency field. indexFromTo := make(map[int32]int32) indexTo := 0 + // Only handle imports and dependencies if there are any. for indexFrom, importPath := range imageFileDescriptor.GetDependency() { path := append(basePath, int32(indexFrom)) - if _, ok := importsRequired[importPath]; ok { + // We check if the import path exists among required imports. If yes, we + // move and then delete from required imports as we go. + if importsRequired != nil && importsRequired.index(importPath) != -1 { sourcePathRemapper.markMoved(path, int32(indexTo)) indexFromTo[int32(indexFrom)] = int32(indexTo) imageFileDescriptor.Dependency[indexTo] = importPath indexTo++ - // markDeleted them as we go, so we know which ones weren't in the list - delete(importsRequired, importPath) + // delete them as we go, so we know which ones weren't in the list + importsRequired.delete(importPath) } else { + // Path did not exist in required imports, we mark as deleted. sourcePathRemapper.markDeleted(path) } } @@ -284,9 +288,12 @@ func ImageFilteredByTypesWithOptions(image bufimage.Image, types []string, opts // Add any other imports (which may not have been in the list because // they were picked up via a public import). The filtered files will not // use public imports. - for importPath := range importsRequired { - imageFileDescriptor.Dependency = append(imageFileDescriptor.Dependency, importPath) + // The imports are added in the order they are encountered when importing + // to maintain a deterministic ordering. + if importsRequired != nil { + imageFileDescriptor.Dependency = append(imageFileDescriptor.Dependency, importsRequired.keys()...) } + imageFileDescriptor.PublicDependency = nil sourcePathRemapper.markDeleted([]int32{filePublicDependencyTag}) @@ -458,9 +465,9 @@ type transitiveClosure struct { // entire package being included). The above fields are used to filter the // contents of files. But files named in this set will not be filtered. completeFiles map[string]struct{} - // The set of imports for each file. This allows for re-writing imports + // The ordered set of imports for each file. This allows for re-writing imports // for files whose contents have been pruned. - imports map[string]map[string]struct{} + imports map[string]*orderedImports } type closureInclusionMode int @@ -485,7 +492,7 @@ func newTransitiveClosure() *transitiveClosure { elements: map[namedDescriptor]closureInclusionMode{}, files: map[string]struct{}{}, completeFiles: map[string]struct{}{}, - imports: map[string]map[string]struct{}{}, + imports: map[string]*orderedImports{}, } } @@ -495,10 +502,10 @@ func (t *transitiveClosure) addImport(fromPath, toPath string) { } imps := t.imports[fromPath] if imps == nil { - imps = map[string]struct{}{} + imps = newOrderedImports() t.imports[fromPath] = imps } - imps[toPath] = struct{}{} + imps.add(toPath) } func (t *transitiveClosure) addFile(file string, imageIndex *imageIndex, opts *imageFilterOptions) error { @@ -1000,3 +1007,55 @@ func stripSourceRetentionOptionsFromFile(imageFile bufimage.ImageFile) (bufimage imageFile.UnusedDependencyIndexes(), ) } + +// orderedImports is a structure to maintain an ordered set of imports. This is needed +// because we want to be able to iterate through imports in a deterministic way when filtering +// the image. +type orderedImports struct { + pathToIndex map[string]int + paths []string +} + +// newOrderedImports creates a new orderedImports structure. +func newOrderedImports() *orderedImports { + return &orderedImports{ + pathToIndex: map[string]int{}, + } +} + +// index returns the index for a given path. If the path does not exist in index map, -1 +// is returned and should be considered deleted. +func (o *orderedImports) index(path string) int { + if index, ok := o.pathToIndex[path]; ok { + return index + } + return -1 +} + +// add appends a path to the paths list and the index in the map. If a key already exists, +// then this is a no-op. +func (o *orderedImports) add(path string) { + if _, ok := o.pathToIndex[path]; !ok { + o.pathToIndex[path] = len(o.paths) + o.paths = append(o.paths, path) + } +} + +// delete removes a key from the index map of ordered imports. If a non-existent path is +// set for deletion, then this is a no-op. +// Note that the path is not removed from the paths list. If you want to iterate through +// the paths, use keys() to get all non-deleted keys. +func (o *orderedImports) delete(path string) { + delete(o.pathToIndex, path) +} + +// keys provides all non-deleted keys from the ordered imports. +func (o *orderedImports) keys() []string { + keys := make([]string, 0, len(o.pathToIndex)) + for _, path := range o.paths { + if _, ok := o.pathToIndex[path]; ok { + keys = append(keys, path) + } + } + return keys +}