From aa456ba0f1007a55bf9fa96faaa93a051ff0fcb5 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 26 Feb 2026 22:35:41 +0100 Subject: [PATCH 01/31] Implemented new user config setting for specifying Regolith's 'tmp' directory path: - GetAbsoluteWorkingDirectory, now tries to read the path from config first. If not specified it uses the old '.regolith' path. - When 'tmp_dir' is relative it uses project's root as the base path. - Added new user config option 'tmp_dir'. - Updated 'regolith config' command to add options for setting/reseting 'tmp_dir' setting. - Added proper error handling to GetAbsoluteWorkingDirectory function - Updated all filters after adding option to return error from GetAbsoluteWorkingDirectory. - All code that refers to the 'tmp' directory, now uses the GetAbsoluteWorkingDirectory function for that purpose. --- regolith/errors.go | 4 +++- regolith/export.go | 36 ++++++++++++++++++++++++------------ regolith/filter_deno.go | 9 ++++++--- regolith/filter_dotnet.go | 9 ++++++--- regolith/filter_exe.go | 10 ++++++---- regolith/filter_java.go | 9 ++++++--- regolith/filter_nim.go | 9 ++++++--- regolith/filter_nodejs.go | 9 ++++++--- regolith/filter_python.go | 7 +++++-- regolith/filter_shell.go | 9 ++++++--- regolith/main_functions.go | 10 ++++++++++ regolith/profile.go | 31 +++++++++++++++++-------------- regolith/user_config.go | 18 ++++++++++++++++++ regolith/utils.go | 27 +++++++++++++++++++++++---- 14 files changed, 142 insertions(+), 55 deletions(-) diff --git a/regolith/errors.go b/regolith/errors.go index 8d32175f..73caabaf 100644 --- a/regolith/errors.go +++ b/regolith/errors.go @@ -194,7 +194,7 @@ const ( // Error used on attempt to access user config property that is not known // to Regolith. invalidUserConfigPropertyError = "Invalid user configuration property:\n" + - "Property name: %s\n" + "Property name: %s" // Error used when the getGlobalUserConfigPath function fails getGlobalUserConfigPathError = "Failed to get global user_config.json path" @@ -268,4 +268,6 @@ const ( loadEnvFileFromArgError = "Failed to the file with environment variables:\n" + "File path: %s" + + getAbsoluteWorkingDirectoryError = "Failed to get the absolute path of the working directory for filter execution." ) diff --git a/regolith/export.go b/regolith/export.go index 56278414..b093126a 100644 --- a/regolith/export.go +++ b/regolith/export.go @@ -327,25 +327,29 @@ func exportProjectRpAndBp(profile Profile, rpPath, bpPath string, ctx RunContext } } MeasureStart("Export - MoveOrCopy") + absWorkingDir, err := GetAbsoluteWorkingDirectory(dotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } var wg sync.WaitGroup packsData := []struct { - packPath string - tmpPath string - packType string + packPath string + subpathInTmp string + packType string }{ - {bpPath, "tmp/BP", "behavior"}, - {rpPath, "tmp/RP", "resource"}, + {bpPath, "BP", "behavior"}, + {rpPath, "RP", "resource"}, } errChan := make(chan error, len(packsData)) for _, packData := range packsData { - packPath, tmpPath, packType := packData.packPath, packData.tmpPath, packData.packType + packPath, subpathInTmp, packType := packData.packPath, packData.subpathInTmp, packData.packType wg.Go(func() { Logger.Infof("Exporting %s pack to \"%s\".", packType, packPath) var e error if IsExperimentEnabled(SizeTimeCheck) { - e = SyncDirectories(filepath.Join(dotRegolithPath, tmpPath), packPath, exportTarget.ReadOnly) + e = SyncDirectories(filepath.Join(absWorkingDir, subpathInTmp), packPath, exportTarget.ReadOnly) } else { - e = MoveOrCopy(filepath.Join(dotRegolithPath, tmpPath), packPath, exportTarget.ReadOnly, true) + e = MoveOrCopy(filepath.Join(absWorkingDir, subpathInTmp), packPath, exportTarget.ReadOnly, true) } if e != nil { errChan <- burrito.WrapErrorf(e, "Failed to export %s pack.", packType) @@ -421,6 +425,10 @@ func exportProjectData(profile Profile, ctx RunContext) error { return burrito.WrapErrorf(err, newRevertibleFsOperationsError, backupPath) } // Export data + absWorkingDir, err := GetAbsoluteWorkingDirectory(dotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } for _, exportedFilterName := range exportedFilterNames { // Clear export target targetPath := filepath.Join(dataPath, exportedFilterName) @@ -445,7 +453,7 @@ func exportProjectData(profile Profile, ctx RunContext) error { } else { return burrito.WrapErrorf(err, osStatErrorAny, targetPath) } - sourcePath := filepath.Join(dotRegolithPath, "tmp/data", exportedFilterName) + sourcePath := filepath.Join(absWorkingDir, "data", exportedFilterName) // If source path doesn't exist, skip if _, err := os.Stat(sourcePath); os.IsNotExist(err) { continue @@ -516,10 +524,14 @@ func InplaceExportProject( } } // Move files from tmp to RP, BP and data + absWorkingDir, err := GetAbsoluteWorkingDirectory(dotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } moveFiles := [][2]string{ - {filepath.Join(dotRegolithPath, "tmp/RP"), config.ResourceFolder}, - {filepath.Join(dotRegolithPath, "tmp/BP"), config.BehaviorFolder}, - {filepath.Join(dotRegolithPath, "tmp/data"), config.DataPath}, + {filepath.Join(absWorkingDir, "RP"), config.ResourceFolder}, + {filepath.Join(absWorkingDir, "BP"), config.BehaviorFolder}, + {filepath.Join(absWorkingDir, "data"), config.DataPath}, } for _, moveFile := range moveFiles { source, target := moveFile[0], moveFile[1] diff --git a/regolith/filter_deno.go b/regolith/filter_deno.go index be450672..cfb797a9 100644 --- a/regolith/filter_deno.go +++ b/regolith/filter_deno.go @@ -35,7 +35,10 @@ func DenoFilterDefinitionFromObject(id string, obj map[string]any) (*DenoFilterD } func (f *DenoFilter) run(context RunContext) error { - // Run filter + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } if len(f.Settings) == 0 { err := RunSubProcess( "deno", @@ -46,7 +49,7 @@ func (f *DenoFilter) run(context RunContext) error { f.Arguments..., ), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { @@ -62,7 +65,7 @@ func (f *DenoFilter) run(context RunContext) error { f.Definition.Script, string(jsonSettings)}, f.Arguments...), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { diff --git a/regolith/filter_dotnet.go b/regolith/filter_dotnet.go index a8702d5e..d599322f 100644 --- a/regolith/filter_dotnet.go +++ b/regolith/filter_dotnet.go @@ -39,7 +39,10 @@ func (f *DotNetFilter) Run(context RunContext) (bool, error) { } func (f *DotNetFilter) run(context RunContext) error { - // Run the filter + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } if len(f.Settings) == 0 { err := RunSubProcess( "dotnet", @@ -51,7 +54,7 @@ func (f *DotNetFilter) run(context RunContext) error { f.Arguments..., ), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { @@ -68,7 +71,7 @@ func (f *DotNetFilter) run(context RunContext) error { f.Arguments..., ), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { diff --git a/regolith/filter_exe.go b/regolith/filter_exe.go index b6154da6..169c261b 100644 --- a/regolith/filter_exe.go +++ b/regolith/filter_exe.go @@ -75,19 +75,21 @@ func (f *ExeFilter) run( settings map[string]any, context RunContext, ) error { - var err error = nil + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } if len(settings) == 0 { err = executeExeFile(f.Id, f.Definition.Exe, f.Arguments, context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath)) + absWorkingDir) } else { jsonSettings, _ := json.Marshal(settings) err = executeExeFile(f.Id, f.Definition.Exe, append([]string{string(jsonSettings)}, f.Arguments...), - context.AbsoluteLocation, GetAbsoluteWorkingDirectory( - context.DotRegolithPath)) + context.AbsoluteLocation, absWorkingDir) } if err != nil { return burrito.WrapErrorf( diff --git a/regolith/filter_java.go b/regolith/filter_java.go index 27e2e099..d0e7e73d 100644 --- a/regolith/filter_java.go +++ b/regolith/filter_java.go @@ -51,7 +51,10 @@ func (f *JavaFilter) Run(context RunContext) (bool, error) { } func (f *JavaFilter) run(context RunContext) error { - // Run the filter + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } if len(f.Settings) == 0 { err := RunSubProcess( "java", @@ -63,7 +66,7 @@ func (f *JavaFilter) run(context RunContext) error { f.Arguments..., ), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { @@ -80,7 +83,7 @@ func (f *JavaFilter) run(context RunContext) error { f.Arguments..., ), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { diff --git a/regolith/filter_nim.go b/regolith/filter_nim.go index 1c6e83e9..66f1ebc6 100644 --- a/regolith/filter_nim.go +++ b/regolith/filter_nim.go @@ -53,7 +53,10 @@ func NimFilterDefinitionFromObject( } func (f *NimFilter) run(context RunContext) error { - // Run filter + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } if len(f.Settings) == 0 { err := RunSubProcess( "nim", @@ -63,7 +66,7 @@ func (f *NimFilter) run(context RunContext) error { f.Arguments..., ), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { @@ -80,7 +83,7 @@ func (f *NimFilter) run(context RunContext) error { string(jsonSettings)}, f.Arguments...), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { diff --git a/regolith/filter_nodejs.go b/regolith/filter_nodejs.go index 9a0bb85a..5959d96d 100644 --- a/regolith/filter_nodejs.go +++ b/regolith/filter_nodejs.go @@ -51,7 +51,10 @@ func NodeJSFilterDefinitionFromObject(id string, obj map[string]any) (*NodeJSFil } func (f *NodeJSFilter) run(context RunContext) error { - // Run filter + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } if len(f.Settings) == 0 { err := RunSubProcess( "node", @@ -61,7 +64,7 @@ func (f *NodeJSFilter) run(context RunContext) error { f.Arguments..., ), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { @@ -76,7 +79,7 @@ func (f *NodeJSFilter) run(context RunContext) error { f.Definition.Script, string(jsonSettings)}, f.Arguments...), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { diff --git a/regolith/filter_python.go b/regolith/filter_python.go index 3a7f69ab..59f997b1 100644 --- a/regolith/filter_python.go +++ b/regolith/filter_python.go @@ -54,7 +54,10 @@ func PythonFilterDefinitionFromObject(id string, obj map[string]any) (*PythonFil } func (f *PythonFilter) run(context RunContext) error { - // Run filter + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } pythonCommand, err := findPython() if err != nil { return burrito.PassError(err) @@ -94,7 +97,7 @@ func (f *PythonFilter) run(context RunContext) error { } err = RunSubProcess( pythonCommand, args, context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id)) if err != nil { return burrito.WrapError(err, "Failed to run Python script.") diff --git a/regolith/filter_shell.go b/regolith/filter_shell.go index dcac6d91..65a38667 100644 --- a/regolith/filter_shell.go +++ b/regolith/filter_shell.go @@ -81,19 +81,22 @@ func (f *ShellFilter) run( settings map[string]any, context RunContext, ) error { - var err error = nil + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } if len(settings) == 0 { err = executeCommand(f.Id, f.Definition.Command, f.Arguments, context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath)) + absWorkingDir) } else { jsonSettings, _ := json.Marshal(settings) err = executeCommand(f.Id, f.Definition.Command, append([]string{string(jsonSettings)}, f.Arguments...), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath)) + absWorkingDir) } if err != nil { return burrito.WrapError(err, "Failed to run shell command.") diff --git a/regolith/main_functions.go b/regolith/main_functions.go index b51610d3..95e282d4 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -757,6 +757,11 @@ func manageUserConfigEdit(index int, key, value string) error { resolversSet[resolver] = struct{}{} } } + case "tmp_dir": + if index != -1 { + return burrito.WrappedError("Cannot use --index with non-array property.") + } + userConfig.TmpDir = &value default: return burrito.WrappedErrorf(invalidUserConfigPropertyError, key) } @@ -799,6 +804,11 @@ func manageUserConfigDelete(index int, key string) error { userConfig.Resolvers[:index], userConfig.Resolvers[index+1:]...) } + case "tmp_dir": + if index != -1 { + return burrito.WrappedError("Cannot use --index with non-array property.") + } + userConfig.TmpDir = nil default: return burrito.WrappedErrorf(invalidUserConfigPropertyError, key) } diff --git a/regolith/profile.go b/regolith/profile.go index 1bfdad1c..fd2a523c 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -121,9 +121,12 @@ func SetupTmpFiles(context RunContext) error { start := time.Now() useSizeTimeCheck := IsExperimentEnabled(SizeTimeCheck) useSymlinkExport := IsExperimentEnabled(SymlinkExport) - tmpPath := filepath.Join(dotRegolithPath, "tmp") - bpTmpPath := filepath.Join(tmpPath, "BP") - rpTmpPath := filepath.Join(tmpPath, "RP") + absTmpPath, err := GetAbsoluteWorkingDirectory(dotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } + bpTmpPath := filepath.Join(absTmpPath, "BP") + rpTmpPath := filepath.Join(absTmpPath, "RP") // Check if should create symlinks, if yes load bp and rp paths var bpExportPath, rpExportPath string @@ -165,17 +168,17 @@ func SetupTmpFiles(context RunContext) error { // Clean the temporary directory isRegularRun := !useSizeTimeCheck && !useSymlinkExport if isRegularRun || shouldCreateSymlinks { - Logger.Debugf("Cleaning \"%s\"", tmpPath) - err := os.RemoveAll(tmpPath) + Logger.Debugf("Cleaning \"%s\"", absTmpPath) + err := os.RemoveAll(absTmpPath) if err != nil { - return burrito.WrapErrorf(err, osRemoveError, tmpPath) + return burrito.WrapErrorf(err, osRemoveError, absTmpPath) } } // Prepare temp path root - err := os.MkdirAll(tmpPath, 0755) + err = os.MkdirAll(absTmpPath, 0755) if err != nil { - return burrito.WrapErrorf(err, osMkdirError, tmpPath) + return burrito.WrapErrorf(err, osMkdirError, absTmpPath) } // Create symlinks @@ -199,22 +202,22 @@ func SetupTmpFiles(context RunContext) error { } // Create symlinks - if err := createDirLink(filepath.Join(tmpPath, "BP"), bpExportPath); err != nil { - return burrito.WrapErrorf(err, createDirLinkError, filepath.Join(tmpPath, "BP"), bpExportPath) + if err := createDirLink(filepath.Join(absTmpPath, "BP"), bpExportPath); err != nil { + return burrito.WrapErrorf(err, createDirLinkError, filepath.Join(absTmpPath, "BP"), bpExportPath) } - if err := createDirLink(filepath.Join(tmpPath, "RP"), rpExportPath); err != nil { - return burrito.WrapErrorf(err, createDirLinkError, filepath.Join(tmpPath, "RP"), rpExportPath) + if err := createDirLink(filepath.Join(absTmpPath, "RP"), rpExportPath); err != nil { + return burrito.WrapErrorf(err, createDirLinkError, filepath.Join(absTmpPath, "RP"), rpExportPath) } } // Copy the contents of the 'regolith' folder to '[dotRegolithPath]/tmp' - Logger.Debugf("Copying project files to \"%s\"", tmpPath) + Logger.Debugf("Copying project files to \"%s\"", absTmpPath) // Avoid repetitive code of preparing ResourceFolder, BehaviorFolder // and DataPath with a closure setupTmpDirectory := func( path, shortName, descriptiveName string, ) error { - p := filepath.Join(tmpPath, shortName) + p := filepath.Join(absTmpPath, shortName) if path != "" { stats, err := os.Stat(path) if err != nil { diff --git a/regolith/user_config.go b/regolith/user_config.go index f3274a6b..9af79b2f 100644 --- a/regolith/user_config.go +++ b/regolith/user_config.go @@ -42,6 +42,11 @@ type UserConfig struct { // FilterCacheUpdateCooldown is a cooldown duration, to not update resolver cache too often. FilterCacheUpdateCooldown *string `json:"filter_cache_update_cooldown,omitempty"` + + // TmpDir is optional path for setting where regolith should create the tmp directory + // for running filters. When not set, the tmp directory will be placed inside + // the project in .regolith directory. + TmpDir *string `json:"tmp_dir,omitempty"` } func NewUserConfig() *UserConfig { @@ -51,6 +56,7 @@ func NewUserConfig() *UserConfig { Resolvers: []string{}, ResolverCacheUpdateCooldown: nil, FilterCacheUpdateCooldown: nil, + TmpDir: nil, } } @@ -64,6 +70,8 @@ func (u *UserConfig) String() string { result += "\n" + extra extra, _ = u.stringPropertyValue("filter_cache_update_cooldown") result += "\n" + extra + extra, _ = u.stringPropertyValue("tmp_dir") + result += "\n" + extra return result } @@ -104,6 +112,12 @@ func (u *UserConfig) stringPropertyValue(name string) (string, error) { value = fmt.Sprintf("%v", *u.FilterCacheUpdateCooldown) } return fmt.Sprintf("filter_cache_update_cooldown: %v", value), nil + case "tmp_dir": + value := "null" + if u.TmpDir != nil { + value = fmt.Sprintf("%v", *u.TmpDir) + } + return fmt.Sprintf("tmp_dir: %v", value), nil } return "", burrito.WrapErrorf(nil, invalidUserConfigPropertyError, name) } @@ -126,6 +140,10 @@ func (u *UserConfig) fillDefaults() { u.FilterCacheUpdateCooldown = new(string) *u.FilterCacheUpdateCooldown = "5m" } + if u.TmpDir == nil { + u.TmpDir = new(string) + *u.TmpDir = "" + } // Make sure resolvers is not nil and append the default resolver if u.Resolvers == nil { u.Resolvers = []string{} diff --git a/regolith/utils.go b/regolith/utils.go index 6744889d..b1afc570 100644 --- a/regolith/utils.go +++ b/regolith/utils.go @@ -103,10 +103,29 @@ func NotImplementedError(text string) error { return burrito.WrappedError(text) } -// GetAbsoluteWorkingDirectory returns an absolute path to [dotRegolithPath]/tmp -func GetAbsoluteWorkingDirectory(dotRegolithPath string) string { - absoluteWorkingDir, _ := filepath.Abs(filepath.Join(dotRegolithPath, "tmp")) - return absoluteWorkingDir +// GetWorkingDirectory returns the working directory for Regolith (the tmp path). +// [dotRegolithPath]/tmp by default or a path from user config, with +// /tmp appended to it. The returned path is not absolute. +func GetAbsoluteWorkingDirectory(dotRegolithPath string) (string, error) { + userConfig, err := getCombinedUserConfig() + if err != nil { + return "", burrito.WrapError(err, getUserConfigError) + } + if userConfig.TmpDir == nil { + // Should never happen - getComvinedUserConfig() fills the defaults + return "", burrito.WrappedError("tmp_dir is null in user config") + } + tmpDir := filepath.Join(*userConfig.TmpDir, "tmp") + if !filepath.IsAbs(tmpDir) { + tmpDir = filepath.Join(dotRegolithPath, tmpDir) + absTmpDir, err := filepath.Abs(tmpDir) + if err != nil { + return "", burrito.WrapErrorf(err, filepathAbsError, tmpDir) + } + return absTmpDir, nil + } + // else IsAbs == true: clean and return + return filepath.Clean(tmpDir), nil } // CreateEnvironmentVariables creates an array of environment variables including custom ones From b3a51f72d06db845935f5653a3873ec783fa1288 Mon Sep 17 00:00:00 2001 From: PaoeniDev Date: Wed, 15 Apr 2026 03:14:18 +0700 Subject: [PATCH 02/31] feat: add Bun filter runner support --- regolith/filter.go | 6 +++ regolith/filter_bun.go | 118 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 regolith/filter_bun.go diff --git a/regolith/filter.go b/regolith/filter.go index 4cbec331..e8f9a9de 100644 --- a/regolith/filter.go +++ b/regolith/filter.go @@ -303,6 +303,12 @@ var filterInstallerFactories = map[string]filterInstallerFactory{ }, name: "NodeJs", }, + "bun": { + constructor: func(id string, obj map[string]any) (FilterInstaller, error) { + return BunFilterDefinitionFromObject(id, obj) + }, + name: "Bun", + }, "python": { constructor: func(id string, obj map[string]any) (FilterInstaller, error) { return PythonFilterDefinitionFromObject(id, obj) diff --git a/regolith/filter_bun.go b/regolith/filter_bun.go new file mode 100644 index 00000000..eed28c4a --- /dev/null +++ b/regolith/filter_bun.go @@ -0,0 +1,118 @@ +package regolith + +import ( + "encoding/json" + "os" + "os/exec" + "strings" + + "github.com/Bedrock-OSS/go-burrito/burrito" +) + +type BunFilterDefinition struct { + FilterDefinition + Script string `json:"script,omitempty"` +} + +type BunFilter struct { + Filter + Definition BunFilterDefinition `json:"-"` +} + +func BunFilterDefinitionFromObject(id string, obj map[string]any) (*BunFilterDefinition, error) { + filter := &BunFilterDefinition{FilterDefinition: *FilterDefinitionFromObject(id)} + scriptObj, ok := obj["script"] + if !ok { + return nil, burrito.WrappedErrorf(jsonPropertyMissingError, "script") + } + script, ok := scriptObj.(string) + if !ok { + return nil, burrito.WrappedErrorf( + jsonPropertyTypeError, "script", "string") + } + filter.Script = script + return filter, nil +} + +func (f *BunFilter) run(context RunContext) error { + // Run filter + if len(f.Settings) == 0 { + err := RunSubProcess( + "bun", + append([]string{ + "run", + context.AbsoluteLocation + string(os.PathSeparator) + + f.Definition.Script}, + f.Arguments..., + ), + context.AbsoluteLocation, + GetAbsoluteWorkingDirectory(context.DotRegolithPath), + ShortFilterName(f.Id), + ) + if err != nil { + return burrito.WrapError(err, runSubProcessError) + } + } else { + jsonSettings, _ := json.Marshal(f.Settings) + err := RunSubProcess( + "bun", + append([]string{ + "run", + context.AbsoluteLocation + string(os.PathSeparator) + + f.Definition.Script, + string(jsonSettings)}, f.Arguments...), + context.AbsoluteLocation, + GetAbsoluteWorkingDirectory(context.DotRegolithPath), + ShortFilterName(f.Id), + ) + if err != nil { + return burrito.WrapError(err, runSubProcessError) + } + } + return nil +} + +func (f *BunFilter) Run(context RunContext) (bool, error) { + if err := f.run(context); err != nil { + return false, burrito.PassError(err) + } + return context.IsInterrupted(), nil +} + +func (f *BunFilterDefinition) CreateFilterRunner(runConfiguration map[string]any, id string) (FilterRunner, error) { + basicFilter, err := filterFromObject(runConfiguration, id) + if err != nil { + return nil, burrito.WrapError(err, filterFromObjectError) + } + filter := &BunFilter{ + Filter: *basicFilter, + Definition: *f, + } + return filter, nil +} + +func (f *BunFilterDefinition) Check(context RunContext) error { + _, err := exec.LookPath("bun") + if err != nil { + return burrito.WrapError( + err, "Bun not found, download and install it from"+ + " https://bun.com/") + } + cmd, err := exec.Command("bun", "--version").Output() + if err != nil { + return burrito.WrapError(err, "Failed to check Bun version") + } + a := strings.TrimPrefix(strings.Trim(string(cmd), " \n\t"), "v") + Logger.Debugf("Found Bun version %s", a) + return nil +} + +func (f *BunFilterDefinition) InstallDependencies( + parent *RemoteFilterDefinition, dotRegolithPath string, +) error { + return nil +} + +func (f *BunFilter) Check(context RunContext) error { + return f.Definition.Check(context) +} From 8bbd39290df5a75ef88060f71f5a28c606d932ce Mon Sep 17 00:00:00 2001 From: PaoeniDev Date: Wed, 15 Apr 2026 16:25:00 +0700 Subject: [PATCH 03/31] feat: add install filter dependencies using Bun --- regolith/filter_bun.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/regolith/filter_bun.go b/regolith/filter_bun.go index eed28c4a..1999a097 100644 --- a/regolith/filter_bun.go +++ b/regolith/filter_bun.go @@ -107,9 +107,23 @@ func (f *BunFilterDefinition) Check(context RunContext) error { return nil } -func (f *BunFilterDefinition) InstallDependencies( - parent *RemoteFilterDefinition, dotRegolithPath string, -) error { +func (f *BunFilterDefinition) InstallDependencies(parent *RemoteFilterDefinition, dotRegolithPath string) error { + installLocation := "" + // Install dependencies + if parent != nil { + installLocation = parent.GetDownloadPath(dotRegolithPath) + } + Logger.Infof("Downloading dependencies for %s...", f.Id) + if hasPackageJson(installLocation) { + Logger.Info("Installing bun dependencies...") + err := RunSubProcess("bun", []string{"install", "--silent"}, installLocation, installLocation, ShortFilterName(f.Id)) + if err != nil { + return burrito.WrapErrorf( + err, "Failed to run bun and install dependencies."+ + "\nFilter name: %s", f.Id) + } + } + Logger.Infof("Dependencies for %s installed successfully", f.Id) return nil } From 4134fc987742393f849f4ce8374531bc73732a0c Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 16 Apr 2026 14:32:43 +0200 Subject: [PATCH 04/31] When the tmpDir user setting is used with an absolute path, Regolith automatically adds a subpath based on the hash of the working directory to prevent collisions when multiple instances of Regolith run at the same time. --- regolith/utils.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/regolith/utils.go b/regolith/utils.go index b1afc570..b3b4e5a1 100644 --- a/regolith/utils.go +++ b/regolith/utils.go @@ -124,8 +124,22 @@ func GetAbsoluteWorkingDirectory(dotRegolithPath string) (string, error) { } return absTmpDir, nil } - // else IsAbs == true: clean and return - return filepath.Clean(tmpDir), nil + // Path is NOT absolute, this means we're using userConfig.TmpDir, + // instead of using the 'tmp' folder we use a hash from the Regolith's + // working directory to avoid collisions when multiple instances of + // Regolith are running. + + // Get the md5 of the current working directory + projectDir, err := os.Getwd() + if err != nil { + return "", burrito.WrapErrorf(err, osGetwdError) + } + hash := md5.New() + hash.Write([]byte(projectDir)) + hashInBytes := hash.Sum(nil) + projectPathHash := hex.EncodeToString(hashInBytes) + + return filepath.Clean(filepath.Join(*userConfig.TmpDir, projectPathHash)), nil } // CreateEnvironmentVariables creates an array of environment variables including custom ones From 046bca9fc823e4d44071e79c39e900983a3d7aa8 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 16 Apr 2026 20:06:23 +0200 Subject: [PATCH 05/31] Fixed bun filter after creating compilation errors from merging branches #350 and #351 --- regolith/filter_bun.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/regolith/filter_bun.go b/regolith/filter_bun.go index 1999a097..e0612769 100644 --- a/regolith/filter_bun.go +++ b/regolith/filter_bun.go @@ -35,6 +35,10 @@ func BunFilterDefinitionFromObject(id string, obj map[string]any) (*BunFilterDef } func (f *BunFilter) run(context RunContext) error { + absWorkingDir, err := GetAbsoluteWorkingDirectory(context.DotRegolithPath) + if err != nil { + return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) + } // Run filter if len(f.Settings) == 0 { err := RunSubProcess( @@ -46,7 +50,7 @@ func (f *BunFilter) run(context RunContext) error { f.Arguments..., ), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { @@ -62,7 +66,7 @@ func (f *BunFilter) run(context RunContext) error { f.Definition.Script, string(jsonSettings)}, f.Arguments...), context.AbsoluteLocation, - GetAbsoluteWorkingDirectory(context.DotRegolithPath), + absWorkingDir, ShortFilterName(f.Id), ) if err != nil { From c4e7ee2fd6945bd48f9c0ebb4aa78b4f6f122a58 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Sat, 18 Apr 2026 01:56:42 +0200 Subject: [PATCH 06/31] Added userSettingIncorrectIndexUseError. --- regolith/errors.go | 4 ++++ regolith/main_functions.go | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/regolith/errors.go b/regolith/errors.go index 73caabaf..ead52be4 100644 --- a/regolith/errors.go +++ b/regolith/errors.go @@ -270,4 +270,8 @@ const ( "File path: %s" getAbsoluteWorkingDirectoryError = "Failed to get the absolute path of the working directory for filter execution." + + // userSettingIncorrectIndexUseError is used when the user tries to use the --index flag with a non-array property + // when changing the user settings. + userSettingIncorrectIndexUseError = "Cannot use --index with non-array property." ) diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 95e282d4..17d8e99d 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -703,7 +703,7 @@ func manageUserConfigEdit(index int, key, value string) error { switch key { case "use_project_app_data_storage": if index != -1 { - return burrito.WrappedError("Cannot use --index with non-array property.") + return burrito.WrappedError(userSettingIncorrectIndexUseError) } boolValue, err := strconv.ParseBool(value) if err != nil { @@ -713,12 +713,12 @@ func manageUserConfigEdit(index int, key, value string) error { userConfig.UseProjectAppDataStorage = &boolValue case "username": if index != -1 { - return burrito.WrappedError("Cannot use --index with non-array property.") + return burrito.WrappedError(userSettingIncorrectIndexUseError) } userConfig.Username = &value case "resolver_cache_update_cooldown": if index != -1 { - return burrito.WrappedError("Cannot use --index with non-array property.") + return burrito.WrappedError(userSettingIncorrectIndexUseError) } _, err = time.ParseDuration(value) if err != nil { @@ -728,7 +728,7 @@ func manageUserConfigEdit(index int, key, value string) error { userConfig.ResolverCacheUpdateCooldown = &value case "filter_cache_update_cooldown": if index != -1 { - return burrito.WrappedError("Cannot use --index with non-array property.") + return burrito.WrappedError(userSettingIncorrectIndexUseError) } _, err = time.ParseDuration(value) if err != nil { @@ -759,7 +759,7 @@ func manageUserConfigEdit(index int, key, value string) error { } case "tmp_dir": if index != -1 { - return burrito.WrappedError("Cannot use --index with non-array property.") + return burrito.WrappedError(userSettingIncorrectIndexUseError) } userConfig.TmpDir = &value default: From f0e4a4598979cca639f5aecc54612e7163c45705 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Sat, 18 Apr 2026 01:58:12 +0200 Subject: [PATCH 07/31] Fixed a typo in a comment. --- regolith/user_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regolith/user_config.go b/regolith/user_config.go index 9af79b2f..be6a9373 100644 --- a/regolith/user_config.go +++ b/regolith/user_config.go @@ -43,7 +43,7 @@ type UserConfig struct { // FilterCacheUpdateCooldown is a cooldown duration, to not update resolver cache too often. FilterCacheUpdateCooldown *string `json:"filter_cache_update_cooldown,omitempty"` - // TmpDir is optional path for setting where regolith should create the tmp directory + // TmpDir is optional path for setting where Regolith should create the tmp directory // for running filters. When not set, the tmp directory will be placed inside // the project in .regolith directory. TmpDir *string `json:"tmp_dir,omitempty"` From dbfa60b6f639c0e7668410064519bd1e85601b79 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Sat, 18 Apr 2026 03:23:58 +0200 Subject: [PATCH 08/31] Added user settings for specifying the path to the runners for executing filters: - bun - deno - dotnet - java - nim - nimble (for installing nim packages) - node - npm (for installing npm packages) - python (python finds pip using 'python -m pip' command, instead of calling pip directly like it used to) --- regolith/filter_bun.go | 25 +++++-- regolith/filter_deno.go | 18 ++++- regolith/filter_dotnet.go | 18 ++++- regolith/filter_java.go | 18 ++++- regolith/filter_nim.go | 27 +++++-- regolith/filter_nodejs.go | 25 +++++-- regolith/filter_python.go | 24 +++++- regolith/main_functions.go | 96 +++++++++++++++++++++++- regolith/user_config.go | 150 +++++++++++++++++++++++++++++++++++++ 9 files changed, 366 insertions(+), 35 deletions(-) diff --git a/regolith/filter_bun.go b/regolith/filter_bun.go index e0612769..44ff458b 100644 --- a/regolith/filter_bun.go +++ b/regolith/filter_bun.go @@ -39,10 +39,15 @@ func (f *BunFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + bunRunner := *userConfig.BunRunner // Run filter if len(f.Settings) == 0 { err := RunSubProcess( - "bun", + bunRunner, append([]string{ "run", context.AbsoluteLocation + string(os.PathSeparator) + @@ -59,7 +64,7 @@ func (f *BunFilter) run(context RunContext) error { } else { jsonSettings, _ := json.Marshal(f.Settings) err := RunSubProcess( - "bun", + bunRunner, append([]string{ "run", context.AbsoluteLocation + string(os.PathSeparator) + @@ -96,13 +101,18 @@ func (f *BunFilterDefinition) CreateFilterRunner(runConfiguration map[string]any } func (f *BunFilterDefinition) Check(context RunContext) error { - _, err := exec.LookPath("bun") + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + bunRunner := *userConfig.BunRunner + _, err = exec.LookPath(bunRunner) if err != nil { return burrito.WrapError( err, "Bun not found, download and install it from"+ " https://bun.com/") } - cmd, err := exec.Command("bun", "--version").Output() + cmd, err := exec.Command(bunRunner, "--version").Output() if err != nil { return burrito.WrapError(err, "Failed to check Bun version") } @@ -119,8 +129,13 @@ func (f *BunFilterDefinition) InstallDependencies(parent *RemoteFilterDefinition } Logger.Infof("Downloading dependencies for %s...", f.Id) if hasPackageJson(installLocation) { + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + bunRunner := *userConfig.BunRunner Logger.Info("Installing bun dependencies...") - err := RunSubProcess("bun", []string{"install", "--silent"}, installLocation, installLocation, ShortFilterName(f.Id)) + err = RunSubProcess(bunRunner, []string{"install", "--silent"}, installLocation, installLocation, ShortFilterName(f.Id)) if err != nil { return burrito.WrapErrorf( err, "Failed to run bun and install dependencies."+ diff --git a/regolith/filter_deno.go b/regolith/filter_deno.go index cfb797a9..be03ce9d 100644 --- a/regolith/filter_deno.go +++ b/regolith/filter_deno.go @@ -39,9 +39,14 @@ func (f *DenoFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + denoRunner := *userConfig.DenoRunner if len(f.Settings) == 0 { err := RunSubProcess( - "deno", + denoRunner, append([]string{ "run", "--allow-all", context.AbsoluteLocation + string(os.PathSeparator) + @@ -58,7 +63,7 @@ func (f *DenoFilter) run(context RunContext) error { } else { jsonSettings, _ := json.Marshal(f.Settings) err := RunSubProcess( - "deno", + denoRunner, append([]string{ "run", "--allow-all", context.AbsoluteLocation + string(os.PathSeparator) + @@ -95,13 +100,18 @@ func (f *DenoFilterDefinition) CreateFilterRunner(runConfiguration map[string]an } func (f *DenoFilterDefinition) Check(context RunContext) error { - _, err := exec.LookPath("deno") + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + denoRunner := *userConfig.DenoRunner + _, err = exec.LookPath(denoRunner) if err != nil { return burrito.WrapError( err, "Deno not found, download and install it from"+ " https://deno.land/") } - cmd, err := exec.Command("deno", "--version").Output() + cmd, err := exec.Command(denoRunner, "--version").Output() if err != nil { return burrito.WrapError(err, "Failed to check Deno version") } diff --git a/regolith/filter_dotnet.go b/regolith/filter_dotnet.go index d599322f..e24362ef 100644 --- a/regolith/filter_dotnet.go +++ b/regolith/filter_dotnet.go @@ -43,9 +43,14 @@ func (f *DotNetFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + dotnetRunner := *userConfig.DotnetRunner if len(f.Settings) == 0 { err := RunSubProcess( - "dotnet", + dotnetRunner, append( []string{ context.AbsoluteLocation + string(os.PathSeparator) + @@ -63,7 +68,7 @@ func (f *DotNetFilter) run(context RunContext) error { } else { jsonSettings, _ := json.Marshal(f.Settings) err := RunSubProcess( - "dotnet", + dotnetRunner, append( []string{ context.AbsoluteLocation + string(os.PathSeparator) + @@ -98,14 +103,19 @@ func (f *DotNetFilterDefinition) InstallDependencies(*RemoteFilterDefinition, st } func (f *DotNetFilterDefinition) Check(context RunContext) error { - _, err := exec.LookPath("dotnet") + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + dotnetRunner := *userConfig.DotnetRunner + _, err = exec.LookPath(dotnetRunner) if err != nil { return burrito.WrapError( err, ".Net not found, download and install it"+ " from https://dotnet.microsoft.com/download") } - cmd, err := exec.Command("dotnet", "--version").Output() + cmd, err := exec.Command(dotnetRunner, "--version").Output() if err != nil { return burrito.WrapError(err, "Failed to check .Net version") } diff --git a/regolith/filter_java.go b/regolith/filter_java.go index d0e7e73d..bdd35133 100644 --- a/regolith/filter_java.go +++ b/regolith/filter_java.go @@ -55,9 +55,14 @@ func (f *JavaFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + javaRunner := *userConfig.JavaRunner if len(f.Settings) == 0 { err := RunSubProcess( - "java", + javaRunner, append( []string{ "-jar", context.AbsoluteLocation + string(os.PathSeparator) + @@ -75,7 +80,7 @@ func (f *JavaFilter) run(context RunContext) error { } else { jsonSettings, _ := json.Marshal(f.Settings) err := RunSubProcess( - "java", + javaRunner, append( []string{ "-jar", context.AbsoluteLocation + string(os.PathSeparator) + @@ -110,14 +115,19 @@ func (f *JavaFilterDefinition) InstallDependencies(*RemoteFilterDefinition, stri } func (f *JavaFilterDefinition) Check(context RunContext) error { - _, err := exec.LookPath("java") + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + javaRunner := *userConfig.JavaRunner + _, err = exec.LookPath(javaRunner) if err != nil { return burrito.WrapError( err, "Java not found, download and install it"+ " from https://adoptopenjdk.net/") } - cmd, err := exec.Command("java", "-version").Output() + cmd, err := exec.Command(javaRunner, "-version").Output() if err != nil { return burrito.WrapError(err, "Failed to check Java version") } diff --git a/regolith/filter_nim.go b/regolith/filter_nim.go index 66f1ebc6..7fc4ce2e 100644 --- a/regolith/filter_nim.go +++ b/regolith/filter_nim.go @@ -57,9 +57,14 @@ func (f *NimFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + nimRunner := *userConfig.NimRunner if len(f.Settings) == 0 { err := RunSubProcess( - "nim", + nimRunner, append([]string{ "-r", "c", "--hints:off", "--warnings:off", "--mm:orc", context.AbsoluteLocation + string(os.PathSeparator) + f.Definition.Script}, @@ -75,7 +80,7 @@ func (f *NimFilter) run(context RunContext) error { } else { jsonSettings, _ := json.Marshal(f.Settings) err := RunSubProcess( - "nim", + nimRunner, append([]string{ "-r", "c", "--hints:off", "--warnings:off", "--mm:orc", context.AbsoluteLocation + string(os.PathSeparator) + @@ -140,9 +145,14 @@ func (f *NimFilterDefinition) InstallDependencies( } Logger.Debugf("Installing dependencies using nimble in %s", requirementsPath) if hasNimble(requirementsPath) { + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + nimbleRunner := *userConfig.NimbleRunner Logger.Info("Installing nim dependencies...") - err := RunSubProcess( - "nimble", []string{"install", "-d", "-y"}, requirementsPath, requirementsPath, ShortFilterName(f.Id)) + err = RunSubProcess( + nimbleRunner, []string{"install", "-d", "-y"}, requirementsPath, requirementsPath, ShortFilterName(f.Id)) if err != nil { return burrito.WrapErrorf( err, "Failed to run nimble to install dependencies of a filter.\n"+ @@ -155,14 +165,19 @@ func (f *NimFilterDefinition) InstallDependencies( } func (f *NimFilterDefinition) Check(context RunContext) error { - _, err := exec.LookPath("nim") + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + nimRunner := *userConfig.NimRunner + _, err = exec.LookPath(nimRunner) if err != nil { return burrito.WrapError( err, "Nim not found, download and install it from"+ " https://nim-lang.org/") } - cmd, err := exec.Command("nim", "--version").Output() + cmd, err := exec.Command(nimRunner, "--version").Output() if err != nil { return burrito.WrapError(err, "Failed to check Nim version.") } diff --git a/regolith/filter_nodejs.go b/regolith/filter_nodejs.go index 5959d96d..f52cca86 100644 --- a/regolith/filter_nodejs.go +++ b/regolith/filter_nodejs.go @@ -55,9 +55,14 @@ func (f *NodeJSFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + nodeRunner := *userConfig.NodeRunner if len(f.Settings) == 0 { err := RunSubProcess( - "node", + nodeRunner, append([]string{ context.AbsoluteLocation + string(os.PathSeparator) + f.Definition.Script}, @@ -73,7 +78,7 @@ func (f *NodeJSFilter) run(context RunContext) error { } else { jsonSettings, _ := json.Marshal(f.Settings) err := RunSubProcess( - "node", + nodeRunner, append([]string{ context.AbsoluteLocation + string(os.PathSeparator) + f.Definition.Script, @@ -133,8 +138,13 @@ func (f *NodeJSFilterDefinition) InstallDependencies(parent *RemoteFilterDefinit requirementsPath = installPath } if hasPackageJson(requirementsPath) { + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + npmRunner := *userConfig.NpmRunner Logger.Info("Installing npm dependencies...") - err := RunSubProcess("npm", []string{"i", "--no-fund", "--no-audit"}, requirementsPath, requirementsPath, ShortFilterName(f.Id)) + err = RunSubProcess(npmRunner, []string{"i", "--no-fund", "--no-audit"}, requirementsPath, requirementsPath, ShortFilterName(f.Id)) if err != nil { return burrito.WrapErrorf( err, "Failed to run npm and install dependencies."+ @@ -146,13 +156,18 @@ func (f *NodeJSFilterDefinition) InstallDependencies(parent *RemoteFilterDefinit } func (f *NodeJSFilterDefinition) Check(context RunContext) error { - _, err := exec.LookPath("node") + userConfig, err := getCombinedUserConfig() + if err != nil { + return burrito.WrapError(err, getUserConfigError) + } + nodeRunner := *userConfig.NodeRunner + _, err = exec.LookPath(nodeRunner) if err != nil { return burrito.WrapError( err, "NodeJS not found, download and install it from"+ " https://nodejs.org/en/") } - cmd, err := exec.Command("node", "--version").Output() + cmd, err := exec.Command(nodeRunner, "--version").Output() if err != nil { return burrito.WrapError(err, "Failed to check NodeJS version") } diff --git a/regolith/filter_python.go b/regolith/filter_python.go index 59f997b1..f077a356 100644 --- a/regolith/filter_python.go +++ b/regolith/filter_python.go @@ -182,9 +182,9 @@ func (f *PythonFilterDefinition) InstallDependencies( Logger.Info("Installing pip dependencies...") requirementsFolder := filepath.Dir(requirementsFile) err = RunSubProcess( - filepath.Join(venvPath, venvScriptsPath, "pip"+exeSuffix), - []string{"install", "-r", filepath.Base(requirementsFile)}, requirementsFolder, - requirementsFolder, ShortFilterName(f.Id)) + venvPythonCommand, + []string{"-m", "pip", "install", "-r", filepath.Base(requirementsFile)}, + requirementsFolder, requirementsFolder, ShortFilterName(f.Id)) if err != nil { return burrito.WrapErrorf( err, "Couldn't run Pip to install dependencies of %s", @@ -241,8 +241,24 @@ func needsVenv(requirementsFilePath string) bool { return false } +// findPython returns the Python command to use. If PythonRunner is set in the +// user config, it uses that value directly. Otherwise, it falls back to +// trying the platform-specific pythonExeNames list. func findPython() (string, error) { - var err error + userConfig, err := getCombinedUserConfig() + if err != nil { + return "", burrito.WrapError(err, getUserConfigError) + } + if userConfig.PythonRunner != nil { + _, err = exec.LookPath(*userConfig.PythonRunner) + if err == nil { + return *userConfig.PythonRunner, nil + } + return "", burrito.WrappedErrorf( + "Python not found at configured path %q, download and install it from "+ + "https://www.python.org/downloads/", *userConfig.PythonRunner) + } + // Fallback: try platform-specific executable names for _, c := range pythonExeNames { _, err = exec.LookPath(c) if err == nil { diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 17d8e99d..43d504a3 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -762,6 +762,51 @@ func manageUserConfigEdit(index int, key, value string) error { return burrito.WrappedError(userSettingIncorrectIndexUseError) } userConfig.TmpDir = &value + case "bun_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.BunRunner = &value + case "deno_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.DenoRunner = &value + case "dotnet_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.DotnetRunner = &value + case "java_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.JavaRunner = &value + case "nim_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NimRunner = &value + case "nimble_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NimbleRunner = &value + case "node_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NodeRunner = &value + case "npm_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NpmRunner = &value + case "python_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.PythonRunner = &value default: return burrito.WrappedErrorf(invalidUserConfigPropertyError, key) } @@ -785,12 +830,12 @@ func manageUserConfigDelete(index int, key string) error { switch key { case "use_project_app_data_storage": if index != -1 { - return burrito.WrappedError("Cannot use --index with non-array property.") + return burrito.WrappedError(userSettingIncorrectIndexUseError) } userConfig.UseProjectAppDataStorage = nil case "username": if index != -1 { - return burrito.WrappedError("Cannot use --index with non-array property.") + return burrito.WrappedError(userSettingIncorrectIndexUseError) } userConfig.Username = nil case "resolvers": @@ -806,9 +851,54 @@ func manageUserConfigDelete(index int, key string) error { } case "tmp_dir": if index != -1 { - return burrito.WrappedError("Cannot use --index with non-array property.") + return burrito.WrappedError(userSettingIncorrectIndexUseError) } userConfig.TmpDir = nil + case "bun_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.BunRunner = nil + case "deno_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.DenoRunner = nil + case "dotnet_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.DotnetRunner = nil + case "java_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.JavaRunner = nil + case "nim_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NimRunner = nil + case "nimble_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NimbleRunner = nil + case "node_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NodeRunner = nil + case "npm_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NpmRunner = nil + case "python_runner": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.PythonRunner = nil default: return burrito.WrappedErrorf(invalidUserConfigPropertyError, key) } diff --git a/regolith/user_config.go b/regolith/user_config.go index be6a9373..2d7ee1c1 100644 --- a/regolith/user_config.go +++ b/regolith/user_config.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/Bedrock-OSS/go-burrito/burrito" ) @@ -47,6 +48,35 @@ type UserConfig struct { // for running filters. When not set, the tmp directory will be placed inside // the project in .regolith directory. TmpDir *string `json:"tmp_dir,omitempty"` + + // BunRunner is optional path for Regolith to look for Bun to run filters. + BunRunner *string `json:"bun_runner,omitempty"` + + // DenoRunner is optional path for Regolith to look for Deno to run filters. + DenoRunner *string `json:"deno_runner,omitempty"` + + // DotnetRunner is optional path for Regolith to look for .Net to run filters. + DotnetRunner *string `json:"dotnet_runner,omitempty"` + + // JavaRunner is optional path for Regolith to look for Java to run filters. + JavaRunner *string `json:"java_runner,omitempty"` + + // NimRunner is optional path for Regolith to look for Nim to run filters. + NimRunner *string `json:"nim_runner,omitempty"` + + // NimbleRunner is optional path for Regolith to look for Nimble to install Nim dependencies. + NimbleRunner *string `json:"nimble_runner,omitempty"` + + // NodeRunner is optional path for Regolith to look for Node to run filters. + NodeRunner *string `json:"node_runner,omitempty"` + + // NpmRunner is optional path for Regolith to look for Npm to install Node dependencies. + NpmRunner *string `json:"npm_runner,omitempty"` + + // PythonRunner is optional path for Regolith to look for Python to run + // filters. When nil, Regolith tries the platform-specific executable names + // (e.g. python3, python). + PythonRunner *string `json:"python_runner,omitempty"` } func NewUserConfig() *UserConfig { @@ -57,6 +87,15 @@ func NewUserConfig() *UserConfig { ResolverCacheUpdateCooldown: nil, FilterCacheUpdateCooldown: nil, TmpDir: nil, + BunRunner: nil, + DenoRunner: nil, + DotnetRunner: nil, + JavaRunner: nil, + NimRunner: nil, + NimbleRunner: nil, + NodeRunner: nil, + NpmRunner: nil, + PythonRunner: nil, } } @@ -72,6 +111,24 @@ func (u *UserConfig) String() string { result += "\n" + extra extra, _ = u.stringPropertyValue("tmp_dir") result += "\n" + extra + extra, _ = u.stringPropertyValue("bun_runner") + result += "\n" + extra + extra, _ = u.stringPropertyValue("deno_runner") + result += "\n" + extra + extra, _ = u.stringPropertyValue("dotnet_runner") + result += "\n" + extra + extra, _ = u.stringPropertyValue("java_runner") + result += "\n" + extra + extra, _ = u.stringPropertyValue("nim_runner") + result += "\n" + extra + extra, _ = u.stringPropertyValue("nimble_runner") + result += "\n" + extra + extra, _ = u.stringPropertyValue("node_runner") + result += "\n" + extra + extra, _ = u.stringPropertyValue("npm_runner") + result += "\n" + extra + extra, _ = u.stringPropertyValue("python_runner") + result += "\n" + extra return result } @@ -118,6 +175,61 @@ func (u *UserConfig) stringPropertyValue(name string) (string, error) { value = fmt.Sprintf("%v", *u.TmpDir) } return fmt.Sprintf("tmp_dir: %v", value), nil + case "bun_runner": + value := "null" + if u.BunRunner != nil { + value = fmt.Sprintf("%v", *u.BunRunner) + } + return fmt.Sprintf("bun_runner: %v", value), nil + case "deno_runner": + value := "null" + if u.DenoRunner != nil { + value = fmt.Sprintf("%v", *u.DenoRunner) + } + return fmt.Sprintf("deno_runner: %v", value), nil + case "dotnet_runner": + value := "null" + if u.DotnetRunner != nil { + value = fmt.Sprintf("%v", *u.DotnetRunner) + } + return fmt.Sprintf("dotnet_runner: %v", value), nil + case "java_runner": + value := "null" + if u.JavaRunner != nil { + value = fmt.Sprintf("%v", *u.JavaRunner) + } + return fmt.Sprintf("java_runner: %v", value), nil + case "nim_runner": + value := "null" + if u.NimRunner != nil { + value = fmt.Sprintf("%v", *u.NimRunner) + } + return fmt.Sprintf("nim_runner: %v", value), nil + case "nimble_runner": + value := "null" + if u.NimbleRunner != nil { + value = fmt.Sprintf("%v", *u.NimbleRunner) + } + return fmt.Sprintf("nimble_runner: %v", value), nil + case "node_runner": + value := "null" + if u.NodeRunner != nil { + value = fmt.Sprintf("%v", *u.NodeRunner) + } + return fmt.Sprintf("node_runner: %v", value), nil + case "npm_runner": + value := "null" + if u.NpmRunner != nil { + value = fmt.Sprintf("%v", *u.NpmRunner) + } + return fmt.Sprintf("npm_runner: %v", value), nil + case "python_runner": + if u.PythonRunner != nil { + return fmt.Sprintf("python_runner: %v", *u.PythonRunner), nil + } + return fmt.Sprintf( + "python_runner: null (attempts to run %s)", + strings.Join(pythonExeNames, ", ")), nil } return "", burrito.WrapErrorf(nil, invalidUserConfigPropertyError, name) } @@ -144,6 +256,44 @@ func (u *UserConfig) fillDefaults() { u.TmpDir = new(string) *u.TmpDir = "" } + + // WARNING: It's CRITICAL to set the default values for the runners, the + // filters code dereferences the values received from + // getCombinedUserConfig() with assumption that they are not nil. + if u.BunRunner == nil { + u.BunRunner = new(string) + *u.BunRunner = "bun" + } + if u.DenoRunner == nil { + u.DenoRunner = new(string) + *u.DenoRunner = "deno" + } + if u.DotnetRunner == nil { + u.DotnetRunner = new(string) + *u.DotnetRunner = "dotnet" + } + if u.JavaRunner == nil { + u.JavaRunner = new(string) + *u.JavaRunner = "java" + } + if u.NimRunner == nil { + u.NimRunner = new(string) + *u.NimRunner = "nim" + } + if u.NimbleRunner == nil { + u.NimbleRunner = new(string) + *u.NimbleRunner = "nimble" + } + if u.NodeRunner == nil { + u.NodeRunner = new(string) + *u.NodeRunner = "node" + } + if u.NpmRunner == nil { + u.NpmRunner = new(string) + *u.NpmRunner = "npm" + } + // PythonRunner intentionally has no default (nil = auto-detect). + // When nil, findPython() falls back to the platform-specific pythonExeNames list. // Make sure resolvers is not nil and append the default resolver if u.Resolvers == nil { u.Resolvers = []string{} From 5a7e869c9469ded3e46284ea5731dca96d5ef556 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Mon, 20 Apr 2026 11:19:03 +0200 Subject: [PATCH 09/31] Added node_runner_override user config option, to let users force Regolith to run Node filters with Deno or Bun. --- regolith/filter.go | 9 +++++++++ regolith/main_functions.go | 16 ++++++++++++++++ regolith/user_config.go | 13 +++++++++++++ 3 files changed, 38 insertions(+) diff --git a/regolith/filter.go b/regolith/filter.go index e8f9a9de..72b87204 100644 --- a/regolith/filter.go +++ b/regolith/filter.go @@ -337,6 +337,15 @@ var filterInstallerFactories = map[string]filterInstallerFactory{ func FilterInstallerFromObject(id string, obj map[string]any) (FilterInstaller, error) { runWith, _ := obj["runWith"].(string) + if runWith == "nodejs" { + userConfig, err := getCombinedUserConfig() + if err != nil { + return nil, burrito.WrapError(err, getUserConfigError) + } + if userConfig.NodeRunnerOverride != nil { + runWith = *userConfig.NodeRunnerOverride + } + } if factory, ok := filterInstallerFactories[runWith]; ok { filter, err := factory.constructor(id, obj) if err != nil { diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 17d8e99d..7533608c 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -762,6 +762,17 @@ func manageUserConfigEdit(index int, key, value string) error { return burrito.WrappedError(userSettingIncorrectIndexUseError) } userConfig.TmpDir = &value + case "node_runner_override": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + if value != "nodejs" && value != "bun" && value != "deno" { + return burrito.WrappedErrorf( + "Invalid value for node_runner_override property.\n"+ + "Value: %s\n"+ + "Allowed values: nodejs, bun, deno", value) + } + userConfig.NodeRunnerOverride = &value default: return burrito.WrappedErrorf(invalidUserConfigPropertyError, key) } @@ -809,6 +820,11 @@ func manageUserConfigDelete(index int, key string) error { return burrito.WrappedError("Cannot use --index with non-array property.") } userConfig.TmpDir = nil + case "node_runner_override": + if index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + userConfig.NodeRunnerOverride = nil default: return burrito.WrappedErrorf(invalidUserConfigPropertyError, key) } diff --git a/regolith/user_config.go b/regolith/user_config.go index be6a9373..6f63b3f6 100644 --- a/regolith/user_config.go +++ b/regolith/user_config.go @@ -47,6 +47,10 @@ type UserConfig struct { // for running filters. When not set, the tmp directory will be placed inside // the project in .regolith directory. TmpDir *string `json:"tmp_dir,omitempty"` + + // NodeRunnerOverride is an option that lets you override the Node runner + // to run filters with Bun or Deno + NodeRunnerOverride *string `json:"node_runner_override,omitempty"` } func NewUserConfig() *UserConfig { @@ -57,6 +61,7 @@ func NewUserConfig() *UserConfig { ResolverCacheUpdateCooldown: nil, FilterCacheUpdateCooldown: nil, TmpDir: nil, + NodeRunnerOverride: nil, } } @@ -72,6 +77,8 @@ func (u *UserConfig) String() string { result += "\n" + extra extra, _ = u.stringPropertyValue("tmp_dir") result += "\n" + extra + extra, _ = u.stringPropertyValue("node_runner_override") + result += "\n" + extra return result } @@ -118,6 +125,12 @@ func (u *UserConfig) stringPropertyValue(name string) (string, error) { value = fmt.Sprintf("%v", *u.TmpDir) } return fmt.Sprintf("tmp_dir: %v", value), nil + case "node_runner_override": + value := "null" + if u.NodeRunnerOverride != nil { + value = fmt.Sprintf("%v", *u.NodeRunnerOverride) + } + return fmt.Sprintf("node_runner_override: %v", value), nil } return "", burrito.WrapErrorf(nil, invalidUserConfigPropertyError, name) } From 5de8c59b94b2155d2e4bceb4a0d6e14ae3016700 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Mon, 20 Apr 2026 12:14:22 +0200 Subject: [PATCH 10/31] Added getRunner function. --- regolith/errors.go | 2 + regolith/filter_bun.go | 15 +++----- regolith/filter_deno.go | 10 ++--- regolith/filter_dotnet.go | 10 ++--- regolith/filter_java.go | 10 ++--- regolith/filter_nim.go | 15 +++----- regolith/filter_nodejs.go | 15 +++----- regolith/filter_python.go | 12 +++--- regolith/user_config.go | 80 ++++++++++++++++++--------------------- 9 files changed, 75 insertions(+), 94 deletions(-) diff --git a/regolith/errors.go b/regolith/errors.go index ead52be4..edad0a4d 100644 --- a/regolith/errors.go +++ b/regolith/errors.go @@ -274,4 +274,6 @@ const ( // userSettingIncorrectIndexUseError is used when the user tries to use the --index flag with a non-array property // when changing the user settings. userSettingIncorrectIndexUseError = "Cannot use --index with non-array property." + + getRunnerError = "Failed to get the path to filter runner." ) diff --git a/regolith/filter_bun.go b/regolith/filter_bun.go index 44ff458b..409757d3 100644 --- a/regolith/filter_bun.go +++ b/regolith/filter_bun.go @@ -39,11 +39,10 @@ func (f *BunFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } - userConfig, err := getCombinedUserConfig() + bunRunner, err := getRunner("bun", "bun") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - bunRunner := *userConfig.BunRunner // Run filter if len(f.Settings) == 0 { err := RunSubProcess( @@ -101,11 +100,10 @@ func (f *BunFilterDefinition) CreateFilterRunner(runConfiguration map[string]any } func (f *BunFilterDefinition) Check(context RunContext) error { - userConfig, err := getCombinedUserConfig() + bunRunner, err := getRunner("bun", "bun") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - bunRunner := *userConfig.BunRunner _, err = exec.LookPath(bunRunner) if err != nil { return burrito.WrapError( @@ -129,11 +127,10 @@ func (f *BunFilterDefinition) InstallDependencies(parent *RemoteFilterDefinition } Logger.Infof("Downloading dependencies for %s...", f.Id) if hasPackageJson(installLocation) { - userConfig, err := getCombinedUserConfig() + bunRunner, err := getRunner("bun", "bun") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - bunRunner := *userConfig.BunRunner Logger.Info("Installing bun dependencies...") err = RunSubProcess(bunRunner, []string{"install", "--silent"}, installLocation, installLocation, ShortFilterName(f.Id)) if err != nil { diff --git a/regolith/filter_deno.go b/regolith/filter_deno.go index be03ce9d..253da2f9 100644 --- a/regolith/filter_deno.go +++ b/regolith/filter_deno.go @@ -39,11 +39,10 @@ func (f *DenoFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } - userConfig, err := getCombinedUserConfig() + denoRunner, err := getRunner("deno", "deno") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - denoRunner := *userConfig.DenoRunner if len(f.Settings) == 0 { err := RunSubProcess( denoRunner, @@ -100,11 +99,10 @@ func (f *DenoFilterDefinition) CreateFilterRunner(runConfiguration map[string]an } func (f *DenoFilterDefinition) Check(context RunContext) error { - userConfig, err := getCombinedUserConfig() + denoRunner, err := getRunner("deno", "deno") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - denoRunner := *userConfig.DenoRunner _, err = exec.LookPath(denoRunner) if err != nil { return burrito.WrapError( diff --git a/regolith/filter_dotnet.go b/regolith/filter_dotnet.go index e24362ef..23a45238 100644 --- a/regolith/filter_dotnet.go +++ b/regolith/filter_dotnet.go @@ -43,11 +43,10 @@ func (f *DotNetFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } - userConfig, err := getCombinedUserConfig() + dotnetRunner, err := getRunner("dotnet", "dotnet") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - dotnetRunner := *userConfig.DotnetRunner if len(f.Settings) == 0 { err := RunSubProcess( dotnetRunner, @@ -103,11 +102,10 @@ func (f *DotNetFilterDefinition) InstallDependencies(*RemoteFilterDefinition, st } func (f *DotNetFilterDefinition) Check(context RunContext) error { - userConfig, err := getCombinedUserConfig() + dotnetRunner, err := getRunner("dotnet", "dotnet") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - dotnetRunner := *userConfig.DotnetRunner _, err = exec.LookPath(dotnetRunner) if err != nil { return burrito.WrapError( diff --git a/regolith/filter_java.go b/regolith/filter_java.go index bdd35133..1174ed92 100644 --- a/regolith/filter_java.go +++ b/regolith/filter_java.go @@ -55,11 +55,10 @@ func (f *JavaFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } - userConfig, err := getCombinedUserConfig() + javaRunner, err := getRunner("java", "java") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - javaRunner := *userConfig.JavaRunner if len(f.Settings) == 0 { err := RunSubProcess( javaRunner, @@ -115,11 +114,10 @@ func (f *JavaFilterDefinition) InstallDependencies(*RemoteFilterDefinition, stri } func (f *JavaFilterDefinition) Check(context RunContext) error { - userConfig, err := getCombinedUserConfig() + javaRunner, err := getRunner("java", "java") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - javaRunner := *userConfig.JavaRunner _, err = exec.LookPath(javaRunner) if err != nil { return burrito.WrapError( diff --git a/regolith/filter_nim.go b/regolith/filter_nim.go index 7fc4ce2e..4610060f 100644 --- a/regolith/filter_nim.go +++ b/regolith/filter_nim.go @@ -57,11 +57,10 @@ func (f *NimFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } - userConfig, err := getCombinedUserConfig() + nimRunner, err := getRunner("nim", "nim") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - nimRunner := *userConfig.NimRunner if len(f.Settings) == 0 { err := RunSubProcess( nimRunner, @@ -145,11 +144,10 @@ func (f *NimFilterDefinition) InstallDependencies( } Logger.Debugf("Installing dependencies using nimble in %s", requirementsPath) if hasNimble(requirementsPath) { - userConfig, err := getCombinedUserConfig() + nimbleRunner, err := getRunner("nimble", "nimble") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - nimbleRunner := *userConfig.NimbleRunner Logger.Info("Installing nim dependencies...") err = RunSubProcess( nimbleRunner, []string{"install", "-d", "-y"}, requirementsPath, requirementsPath, ShortFilterName(f.Id)) @@ -165,11 +163,10 @@ func (f *NimFilterDefinition) InstallDependencies( } func (f *NimFilterDefinition) Check(context RunContext) error { - userConfig, err := getCombinedUserConfig() + nimRunner, err := getRunner("nim", "nim") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - nimRunner := *userConfig.NimRunner _, err = exec.LookPath(nimRunner) if err != nil { return burrito.WrapError( diff --git a/regolith/filter_nodejs.go b/regolith/filter_nodejs.go index f52cca86..0834206b 100644 --- a/regolith/filter_nodejs.go +++ b/regolith/filter_nodejs.go @@ -55,11 +55,10 @@ func (f *NodeJSFilter) run(context RunContext) error { if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) } - userConfig, err := getCombinedUserConfig() + nodeRunner, err := getRunner("node", "node") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - nodeRunner := *userConfig.NodeRunner if len(f.Settings) == 0 { err := RunSubProcess( nodeRunner, @@ -138,11 +137,10 @@ func (f *NodeJSFilterDefinition) InstallDependencies(parent *RemoteFilterDefinit requirementsPath = installPath } if hasPackageJson(requirementsPath) { - userConfig, err := getCombinedUserConfig() + npmRunner, err := getRunner("npm", "npm") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - npmRunner := *userConfig.NpmRunner Logger.Info("Installing npm dependencies...") err = RunSubProcess(npmRunner, []string{"i", "--no-fund", "--no-audit"}, requirementsPath, requirementsPath, ShortFilterName(f.Id)) if err != nil { @@ -156,11 +154,10 @@ func (f *NodeJSFilterDefinition) InstallDependencies(parent *RemoteFilterDefinit } func (f *NodeJSFilterDefinition) Check(context RunContext) error { - userConfig, err := getCombinedUserConfig() + nodeRunner, err := getRunner("node", "node") if err != nil { - return burrito.WrapError(err, getUserConfigError) + return burrito.WrapError(err, getRunnerError) } - nodeRunner := *userConfig.NodeRunner _, err = exec.LookPath(nodeRunner) if err != nil { return burrito.WrapError( diff --git a/regolith/filter_python.go b/regolith/filter_python.go index f077a356..ae422afd 100644 --- a/regolith/filter_python.go +++ b/regolith/filter_python.go @@ -245,18 +245,18 @@ func needsVenv(requirementsFilePath string) bool { // user config, it uses that value directly. Otherwise, it falls back to // trying the platform-specific pythonExeNames list. func findPython() (string, error) { - userConfig, err := getCombinedUserConfig() + pythonRunner, err := getRunner("python", "") if err != nil { - return "", burrito.WrapError(err, getUserConfigError) + return "", burrito.WrapError(err, getRunnerError) } - if userConfig.PythonRunner != nil { - _, err = exec.LookPath(*userConfig.PythonRunner) + if pythonRunner != "" { + _, err = exec.LookPath(pythonRunner) if err == nil { - return *userConfig.PythonRunner, nil + return pythonRunner, nil } return "", burrito.WrappedErrorf( "Python not found at configured path %q, download and install it from "+ - "https://www.python.org/downloads/", *userConfig.PythonRunner) + "https://www.python.org/downloads/", pythonRunner) } // Fallback: try platform-specific executable names for _, c := range pythonExeNames { diff --git a/regolith/user_config.go b/regolith/user_config.go index 2d7ee1c1..0203cc74 100644 --- a/regolith/user_config.go +++ b/regolith/user_config.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/Bedrock-OSS/go-burrito/burrito" ) @@ -224,12 +223,11 @@ func (u *UserConfig) stringPropertyValue(name string) (string, error) { } return fmt.Sprintf("npm_runner: %v", value), nil case "python_runner": + value := "null" if u.PythonRunner != nil { - return fmt.Sprintf("python_runner: %v", *u.PythonRunner), nil + value = fmt.Sprintf("%v", *u.PythonRunner) } - return fmt.Sprintf( - "python_runner: null (attempts to run %s)", - strings.Join(pythonExeNames, ", ")), nil + return fmt.Sprintf("python_runner: %v", value), nil } return "", burrito.WrapErrorf(nil, invalidUserConfigPropertyError, name) } @@ -256,44 +254,6 @@ func (u *UserConfig) fillDefaults() { u.TmpDir = new(string) *u.TmpDir = "" } - - // WARNING: It's CRITICAL to set the default values for the runners, the - // filters code dereferences the values received from - // getCombinedUserConfig() with assumption that they are not nil. - if u.BunRunner == nil { - u.BunRunner = new(string) - *u.BunRunner = "bun" - } - if u.DenoRunner == nil { - u.DenoRunner = new(string) - *u.DenoRunner = "deno" - } - if u.DotnetRunner == nil { - u.DotnetRunner = new(string) - *u.DotnetRunner = "dotnet" - } - if u.JavaRunner == nil { - u.JavaRunner = new(string) - *u.JavaRunner = "java" - } - if u.NimRunner == nil { - u.NimRunner = new(string) - *u.NimRunner = "nim" - } - if u.NimbleRunner == nil { - u.NimbleRunner = new(string) - *u.NimbleRunner = "nimble" - } - if u.NodeRunner == nil { - u.NodeRunner = new(string) - *u.NodeRunner = "node" - } - if u.NpmRunner == nil { - u.NpmRunner = new(string) - *u.NpmRunner = "npm" - } - // PythonRunner intentionally has no default (nil = auto-detect). - // When nil, findPython() falls back to the platform-specific pythonExeNames list. // Make sure resolvers is not nil and append the default resolver if u.Resolvers == nil { u.Resolvers = []string{} @@ -391,3 +351,37 @@ func getGlobalUserConfig() (*UserConfig, error) { } return cachedGlobalUserConfig, nil } + +// getRunner returns the runner path from the user config, or the default +// if the config doesn't specify it. +func getRunner(runnerType, defaultRunner string) (string, error) { + userConfig, err := getCombinedUserConfig() + if err != nil { + return "", burrito.WrapError(err, getUserConfigError) + } + var result *string = nil + switch runnerType { + case "bun": + result = userConfig.BunRunner + case "deno": + result = userConfig.DenoRunner + case "dotnet": + result = userConfig.DotnetRunner + case "java": + result = userConfig.JavaRunner + case "nim": + result = userConfig.NimRunner + case "nimble": + result = userConfig.NimbleRunner + case "node": + result = userConfig.NodeRunner + case "npm": + result = userConfig.NpmRunner + case "python": + result = userConfig.PythonRunner + } + if result != nil { + return *result, nil + } + return defaultRunner, nil +} From 2e2cf1ef64ee0af009f17ec542a6c2468acd6c06 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Sun, 26 Apr 2026 16:51:06 +0200 Subject: [PATCH 11/31] The 'node_runner_override' settings can be set per-filter: - '*' is used as a special wildcard filter name that sets the default runner. It is always printed on the top of the list, and more specific filter names can overwrite it. - Renamed the 'key' arguemt in manageUserConfigEdit() and manageUserConfigDelete() to 'setting' and added new 'key' arguemnt that represents the name the key in a map property (in the 'node_runner_override' it represents the name of a filter). - Removed duplicated code in manageUserConfigEdit() and manageUserConfigDelete() funcitons. - Updated the CLI message of the 'regolith config' command. --- main.go | 14 ++-- regolith/errors.go | 4 + regolith/filter.go | 9 +- regolith/main_functions.go | 167 +++++++++++++++---------------------- regolith/user_config.go | 28 +++++-- 5 files changed, 109 insertions(+), 113 deletions(-) diff --git a/main.go b/main.go index 2a40b2f6..e133acbe 100644 --- a/main.go +++ b/main.go @@ -148,12 +148,14 @@ The behavior of the command changes based on the used flags and the number of pr The cheatsheet below shows the possible combinations of flags and arguments and what they do: Printing all properties: regolith config -Printing specified property: regolith config -Setting property value: regolith config -Deleting a property: regolith config --delete -Appending to a list property: regolith config --append -Replacing item in a list property: regolith config --index -Deleting item in a list property: regolith config --index --delete +Printing specified property: regolith config +Setting property value: regolith config +Deleting a property: regolith config --delete +Appending to a list property: regolith config --append +Replacing item in a list property: regolith config --index +Deleting item in a list property: regolith config --index --delete +Setting a value in a map property: regolith config +Deleting a value in a map property: regolith config --delete The printing commands can take the --full flag to print configuration with the default values included (if they're not defined in the config file). Without the flag, the undefined properties diff --git a/regolith/errors.go b/regolith/errors.go index edad0a4d..7dd29d39 100644 --- a/regolith/errors.go +++ b/regolith/errors.go @@ -275,5 +275,9 @@ const ( // when changing the user settings. userSettingIncorrectIndexUseError = "Cannot use --index with non-array property." + // userSettingIncorrectKeyUseError is used when the user tries to use the --key flag with a non-map property + // when changing the user settings. + userSettingIncorrectKeyUseError = "Cannot use with non-map property." + getRunnerError = "Failed to get the path to filter runner." ) diff --git a/regolith/filter.go b/regolith/filter.go index 72b87204..93bdb874 100644 --- a/regolith/filter.go +++ b/regolith/filter.go @@ -343,7 +343,14 @@ func FilterInstallerFromObject(id string, obj map[string]any) (FilterInstaller, return nil, burrito.WrapError(err, getUserConfigError) } if userConfig.NodeRunnerOverride != nil { - runWith = *userConfig.NodeRunnerOverride + defaultOverride, ok := userConfig.NodeRunnerOverride["*"] + if ok { + runWith = defaultOverride + } + override, ok := userConfig.NodeRunnerOverride[id] + if ok { + runWith = override + } } } if factory, ok := filterInstallerFactories[runWith]; ok { diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 09346d05..59d777f5 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -635,7 +635,7 @@ func UpdateResolvers(debug bool, env string) error { // manageUserConfigPrint is a helper function for ManageConfig used to print // the specified value from the user configuration. -func manageUserConfigPrint(full bool, key string) error { +func manageUserConfigPrint(full bool, setting string) error { var err error // prevent shadowing configPath := "" userConfig := NewUserConfig() @@ -653,9 +653,9 @@ func manageUserConfigPrint(full bool, key string) error { fmt.Printf("\nGLOBAL USER CONFIGURATION: %s\n", configPath) userConfig.fillWithFileData(configPath) } - result, err := userConfig.stringPropertyValue(key) + result, err := userConfig.stringPropertyValue(setting) if err != nil { - return burrito.WrapErrorf(err, invalidUserConfigPropertyError, key) + return burrito.WrapErrorf(err, invalidUserConfigPropertyError, setting) } result = "\t" + strings.ReplaceAll(result, "\n", "\n\t") // Indent fmt.Println(result) @@ -692,7 +692,7 @@ func manageUserConfigPrintAll(full bool) error { // manageUserConfigEdit is a helper function for ManageConfig used to edit // the specified value from the user configuration. -func manageUserConfigEdit(index int, key, value string) error { +func manageUserConfigEdit(setting string, index int, key, value string) error { configPath, err := getGlobalUserConfigPath() if err != nil { return burrito.WrapError(err, getGlobalUserConfigPathError) @@ -700,11 +700,18 @@ func manageUserConfigEdit(index int, key, value string) error { Logger.Infof("Editing user configuration.\n\tPath: %s", configPath) userConfig := NewUserConfig() userConfig.fillWithFileData(configPath) - switch key { + + // Only list properties can use 'index' + if setting != "resolvers" && index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + // Only map properties can use 'key' + if setting != "node_runner_override" && key != "" { + return burrito.WrappedError(userSettingIncorrectKeyUseError) + } + + switch setting { case "use_project_app_data_storage": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } boolValue, err := strconv.ParseBool(value) if err != nil { return burrito.WrapErrorf(err, "Invalid value for boolean property.\n"+ @@ -712,14 +719,8 @@ func manageUserConfigEdit(index int, key, value string) error { } userConfig.UseProjectAppDataStorage = &boolValue case "username": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.Username = &value case "resolver_cache_update_cooldown": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } _, err = time.ParseDuration(value) if err != nil { return burrito.WrapErrorf(err, "Invalid value for duration property.\n"+ @@ -727,9 +728,6 @@ func manageUserConfigEdit(index int, key, value string) error { } userConfig.ResolverCacheUpdateCooldown = &value case "filter_cache_update_cooldown": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } _, err = time.ParseDuration(value) if err != nil { return burrito.WrapErrorf(err, "Invalid value for duration property.\n"+ @@ -758,68 +756,38 @@ func manageUserConfigEdit(index int, key, value string) error { } } case "tmp_dir": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.TmpDir = &value case "node_runner_override": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } if value != "nodejs" && value != "bun" && value != "deno" { return burrito.WrappedErrorf( "Invalid value for node_runner_override property.\n"+ "Value: %s\n"+ "Allowed values: nodejs, bun, deno", value) } - userConfig.NodeRunnerOverride = &value - case "bun_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) + if key == "" { + return burrito.WrappedErrorf("Key is required for setting node_runner_override property.") } + userConfig.NodeRunnerOverride[key] = value + case "bun_runner": userConfig.BunRunner = &value case "deno_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.DenoRunner = &value case "dotnet_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.DotnetRunner = &value case "java_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.JavaRunner = &value case "nim_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.NimRunner = &value case "nimble_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.NimbleRunner = &value case "node_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.NodeRunner = &value case "npm_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.NpmRunner = &value case "python_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.PythonRunner = &value default: - return burrito.WrappedErrorf(invalidUserConfigPropertyError, key) + return burrito.WrappedErrorf(invalidUserConfigPropertyError, setting) } err = userConfig.dump(configPath) if err != nil { @@ -830,7 +798,7 @@ func manageUserConfigEdit(index int, key, value string) error { // manageUserConfigDelete is a helper function for ManageConfig used to delete // the specified value from the user configuration. -func manageUserConfigDelete(index int, key string) error { +func manageUserConfigDelete(setting string, index int, key string) error { configPath, err := getGlobalUserConfigPath() if err != nil { return burrito.WrapError(err, getGlobalUserConfigPathError) @@ -838,16 +806,20 @@ func manageUserConfigDelete(index int, key string) error { Logger.Infof("Editing user configuration.\n\tPath: %s", configPath) userConfig := NewUserConfig() userConfig.fillWithFileData(configPath) - switch key { + + // Only list properties can use 'index' + if setting != "resolvers" && index != -1 { + return burrito.WrappedError(userSettingIncorrectIndexUseError) + } + // Only map properties can use 'key' + if setting != "node_runner_override" && key != "" { + return burrito.WrappedError(userSettingIncorrectKeyUseError) + } + + switch setting { case "use_project_app_data_storage": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.UseProjectAppDataStorage = nil case "username": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.Username = nil case "resolvers": if index == -1 { @@ -861,62 +833,36 @@ func manageUserConfigDelete(index int, key string) error { userConfig.Resolvers[index+1:]...) } case "tmp_dir": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.TmpDir = nil case "node_runner_override": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) + // Don't allow deleting everything because it's too destructive, and + // it could easily destroy someone's config by accident. + if key == "" { + return burrito.WrappedErrorf( + "Providing is required for deleting elements from the " + + "node_runner_override setting.") } - userConfig.NodeRunnerOverride = nil + delete(userConfig.NodeRunnerOverride, key) case "bun_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.BunRunner = nil case "deno_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.DenoRunner = nil case "dotnet_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.DotnetRunner = nil case "java_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.JavaRunner = nil case "nim_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.NimRunner = nil case "nimble_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.NimbleRunner = nil case "node_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.NodeRunner = nil case "npm_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.NpmRunner = nil case "python_runner": - if index != -1 { - return burrito.WrappedError(userSettingIncorrectIndexUseError) - } userConfig.PythonRunner = nil default: - return burrito.WrappedErrorf(invalidUserConfigPropertyError, key) + return burrito.WrappedErrorf(invalidUserConfigPropertyError, setting) } err = userConfig.dump(configPath) if err != nil { @@ -978,11 +924,10 @@ func ManageConfig(debug, full, delete, append bool, index int, args []string, en if full { return burrito.WrappedError("The --full flag is only valid for printing.") } - err = manageUserConfigDelete(index, args[0]) + err = manageUserConfigDelete(args[0], index, "") if err != nil { return burrito.PassError(err) } - return nil } else { if index != -1 { return burrito.WrappedError("The --index flag is not allowed for printing.") @@ -991,11 +936,33 @@ func ManageConfig(debug, full, delete, append bool, index int, args []string, en if err != nil { return burrito.PassError(err) } - return nil } + return nil } else if len(args) == 2 { // 2 ARGUMENTS - Set or append + // Check illegal flags + if full { + return burrito.WrappedError("The --full flag is only valid for printing.") + } + + // Delete + if delete { + err = manageUserConfigDelete(args[0], index, args[1]) + if err != nil { + return burrito.PassError(err) + } + } else { + // Set or append + err = manageUserConfigEdit(args[0], index, "", args[1]) + if err != nil { + return burrito.PassError(err) + } + } + return nil + } else if len(args) == 3 { + // 3 ARGUMENTS - Set (map property) + // Check illegal flags if delete { return burrito.WrappedError("When using --delete, only one argument is allowed.") @@ -1003,9 +970,11 @@ func ManageConfig(debug, full, delete, append bool, index int, args []string, en if full { return burrito.WrappedError("The --full flag is only valid for printing.") } + if append { + return burrito.WrappedError("The --append flag is only valid for array properties.") + } - // Set or append - err = manageUserConfigEdit(index, args[0], args[1]) + err = manageUserConfigEdit(args[0], index, args[1], args[2]) if err != nil { return burrito.PassError(err) } diff --git a/regolith/user_config.go b/regolith/user_config.go index bac4b677..d3d31125 100644 --- a/regolith/user_config.go +++ b/regolith/user_config.go @@ -50,7 +50,7 @@ type UserConfig struct { // NodeRunnerOverride is an option that lets you override the Node runner // to run filters with Bun or Deno - NodeRunnerOverride *string `json:"node_runner_override,omitempty"` + NodeRunnerOverride map[string]string `json:"node_runner_override,omitempty"` // BunRunner is optional path for Regolith to look for Bun to run filters. BunRunner *string `json:"bun_runner,omitempty"` @@ -90,7 +90,7 @@ func NewUserConfig() *UserConfig { ResolverCacheUpdateCooldown: nil, FilterCacheUpdateCooldown: nil, TmpDir: nil, - NodeRunnerOverride: nil, + NodeRunnerOverride: map[string]string{}, BunRunner: nil, DenoRunner: nil, DotnetRunner: nil, @@ -182,11 +182,23 @@ func (u *UserConfig) stringPropertyValue(name string) (string, error) { } return fmt.Sprintf("tmp_dir: %v", value), nil case "node_runner_override": - value := "null" - if u.NodeRunnerOverride != nil { - value = fmt.Sprintf("%v", *u.NodeRunnerOverride) + if len(u.NodeRunnerOverride) == 0 { + return "node_runner_override: {}", nil + } + result := "node_runner_override: \n" + // Print default value first + defaultVal, ok := u.NodeRunnerOverride["*"] + if ok { + result += fmt.Sprintf("\t- * => %v\n", defaultVal) + } + // Other values... + for k, v := range u.NodeRunnerOverride { + if k == "*" { + continue + } + result += fmt.Sprintf("\t- %v => %v\n", k, v) } - return fmt.Sprintf("node_runner_override: %v", value), nil + return result, nil case "bun_runner": value := "null" if u.BunRunner != nil { @@ -267,7 +279,9 @@ func (u *UserConfig) fillDefaults() { u.TmpDir = new(string) *u.TmpDir = "" } - // Make sure resolvers is not nil and append the default resolver + if u.NodeRunnerOverride == nil { + u.NodeRunnerOverride = map[string]string{} + } if u.Resolvers == nil { u.Resolvers = []string{} } From 367634ca12a97f91bddadb93ad66cdf38fac25ad Mon Sep 17 00:00:00 2001 From: FrederoxDev Date: Mon, 27 Apr 2026 12:31:26 +0100 Subject: [PATCH 12/31] --unsafe + handle junctions + parallel sync directories --- go.mod | 2 +- go.sum | 2 + main.go | 1 + regolith/errors.go | 2 + regolith/experiments.go | 5 +- regolith/export.go | 19 ++- regolith/file_system.go | 281 ++++++++++++++++++++++++++++++---------- regolith/profile.go | 54 +++++--- 8 files changed, 265 insertions(+), 101 deletions(-) diff --git a/go.mod b/go.mod index a5255f11..35747602 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,6 @@ require ( go.uber.org/multierr v1.8.0 // indirect golang.org/x/crypto v0.1.0 // indirect golang.org/x/exp v0.0.0-20230131013936-aae9b4e6329d // indirect - golang.org/x/sync v0.8.0 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 0cda49cc..16f60424 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index e133acbe..f1243a63 100644 --- a/main.go +++ b/main.go @@ -410,6 +410,7 @@ func main() { ®olith.EnabledExperiments, "experiments", nil, "Enables experimental features. Currently supported experiments:\n"+ strings.Join(experimentDescs, "\n")) + cmd.Flags().BoolVar(®olith.UnsafeMode, "unsafe", false, "Disables file protection safety checks for faster exports") } // Build and run CLI diff --git a/regolith/errors.go b/regolith/errors.go index 7dd29d39..3920e3b5 100644 --- a/regolith/errors.go +++ b/regolith/errors.go @@ -259,6 +259,8 @@ const ( "Resource pack export path: %s\n" + "Behavior pack export path: %s" + updatedFilesUpdateError = "Failed to create a list of files edited by Regolith." + updatedFilesDumpError = "Failed to update the list of the files edited by Regolith." + "This may cause the next run to fail." diff --git a/regolith/experiments.go b/regolith/experiments.go index 48a84980..53d01634 100644 --- a/regolith/experiments.go +++ b/regolith/experiments.go @@ -35,7 +35,10 @@ var AvailableExperiments = map[Experiment]ExperimentInfo{ SymlinkExport: {"symlink_export", symlinkExportDesc}, } -var EnabledExperiments []string +var ( + EnabledExperiments []string + UnsafeMode bool +) func IsExperimentEnabled(exp Experiment) bool { if EnabledExperiments == nil { diff --git a/regolith/export.go b/regolith/export.go index b093126a..74193cc4 100644 --- a/regolith/export.go +++ b/regolith/export.go @@ -247,12 +247,14 @@ func ExportProject(ctx RunContext) error { // Load edited files MeasureStart("Export - CheckDeletionSafety") editedFiles := LoadEditedFiles(dotRegolithPath) - err = editedFiles.CheckDeletionSafety(rpPath, bpPath) - if err != nil { - return burrito.WrapErrorf( - err, - checkDeletionSafetyError, - rpPath, bpPath) + if !IsExperimentEnabled(SymlinkExport) && !UnsafeMode { + err = editedFiles.CheckDeletionSafety(rpPath, bpPath) + if err != nil { + return burrito.WrapErrorf( + err, + checkDeletionSafetyError, + rpPath, bpPath) + } } // Export RP and BP if necessary if IsExperimentEnabled(SymlinkExport) { @@ -270,12 +272,9 @@ func ExportProject(ctx RunContext) error { return burrito.PassError(err) } MeasureStart("Export - EditedFiles.UpdateFromPaths") - // Update or create edited_files.json err = editedFiles.UpdateFromPaths(rpPath, bpPath) if err != nil { - return burrito.WrapError( - err, - "Failed to create a list of files edited by this 'regolith run'") + return burrito.WrapError(err, updatedFilesUpdateError) } err = editedFiles.Dump(dotRegolithPath) if err != nil { diff --git a/regolith/file_system.go b/regolith/file_system.go index 054b8a00..db3a6676 100644 --- a/regolith/file_system.go +++ b/regolith/file_system.go @@ -2,16 +2,20 @@ package regolith import ( "bytes" + "context" "io" "io/fs" "os" "path/filepath" + "runtime" "sort" "strconv" "strings" + "sync" "time" "github.com/Bedrock-OSS/go-burrito/burrito" + "golang.org/x/sync/errgroup" "github.com/otiai10/copy" ) @@ -851,107 +855,245 @@ func MoveOrCopy( // SyncDirectories copies the source to destination while checking size and modification time. // If the file in the destination is different than the one in the source, it's overwritten, // otherwise it's skipped (the destination file is not modified). + +type syncMetadataCache struct { + m sync.Map +} + +type cachedMeta struct { + info os.FileInfo +} + +func (c *syncMetadataCache) get(path string) os.FileInfo { + if v, ok := c.m.Load(path); ok { + return v.(*cachedMeta).info + } + info, err := os.Stat(path) + if err != nil { + c.m.Store(path, &cachedMeta{info: nil}) + return nil + } + c.m.Store(path, &cachedMeta{info: info}) + return info +} + +func (c *syncMetadataCache) invalidate(path string) { + c.m.Delete(path) +} + +func syncLink(srcPath, dstPath string) error { + linkTarget, err := os.Readlink(srcPath) + if err != nil { + Logger.Debugf("SYNC: Skipping irregular file %s", srcPath) + return nil + } + if err := os.Remove(dstPath); err != nil && !os.IsNotExist(err) { + return burrito.WrapErrorf(err, osRemoveError, dstPath) + } + targetInfo, err := os.Stat(srcPath) + if err == nil && targetInfo.IsDir() { + err = createDirLink(dstPath, linkTarget) + } else { + err = os.Symlink(linkTarget, dstPath) + } + if err != nil { + return burrito.WrapErrorf(err, createDirLinkError, dstPath, linkTarget) + } + return nil +} + func SyncDirectories( source string, destination string, makeReadOnly bool, ) error { - // Make destination parent if not exists destinationParent := filepath.Dir(destination) if err := os.MkdirAll(destinationParent, 0755); err != nil { return burrito.WrapErrorf(err, osMkdirError, destinationParent) } - err := filepath.WalkDir(source, func(srcPath string, d fs.DirEntry, err error) error { - if err != nil { - return err + + cache := &syncMetadataCache{} + maxWorkers := runtime.GOMAXPROCS(0) + if maxWorkers < 4 { + maxWorkers = 4 + } + + // Phase 1: Sync source -> destination (parallel file I/O, sequential traversal) + g, ctx := errgroup.WithContext(context.Background()) + g.SetLimit(maxWorkers) + + var syncErr error + var syncDir func(srcDir, dstDir string) + syncDir = func(srcDir, dstDir string) { + if syncErr != nil { + return } - relPath, err := filepath.Rel(source, srcPath) - if err != nil { - return burrito.WrapErrorf(err, filepathRelError, source, srcPath) + if cache.get(dstDir) == nil { + if err := os.MkdirAll(dstDir, 0755); err != nil { + syncErr = burrito.WrapErrorf(err, osMkdirError, dstDir) + return + } } - destPath := filepath.Join(destination, relPath) - destInfo, err := os.Stat(destPath) - if err != nil && !os.IsNotExist(err) { - return burrito.WrapErrorf(err, osStatErrorAny, destPath) - } - info, ierr := d.Info() - if ierr != nil { - return ierr + entries, err := os.ReadDir(srcDir) + if err != nil { + syncErr = burrito.WrapErrorf(err, osReadDirError, srcDir) + return } - if (err != nil && os.IsNotExist(err)) || info.ModTime() != destInfo.ModTime() || info.Size() != destInfo.Size() { - if d.IsDir() { - return os.MkdirAll(destPath, info.Mode()) + + for _, entry := range entries { + if syncErr != nil || ctx.Err() != nil { + return } - Logger.Debugf("SYNC: Copying file %s to %s", srcPath, destPath) - // If file exists, we need to remove it first to avoid permission issues when it's - // read-only - if destInfo != nil { - err = os.Remove(destPath) - if err != nil { - return burrito.WrapErrorf(err, osRemoveError, destPath) + srcPath := filepath.Join(srcDir, entry.Name()) + dstPath := filepath.Join(dstDir, entry.Name()) + + if entry.Type()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { + g.Go(func() error { + if ctx.Err() != nil { + return ctx.Err() + } + return syncLink(srcPath, dstPath) + }) + continue + } + + srcMeta := cache.get(srcPath) + if srcMeta != nil && srcMeta.IsDir() { + dstMeta := cache.get(dstPath) + if dstMeta != nil && !dstMeta.IsDir() { + if err := os.Remove(dstPath); err != nil { + syncErr = burrito.WrapErrorf(err, osRemoveError, dstPath) + return + } + } + if dstMeta != nil && dstMeta.IsDir() { + linfo, lerr := os.Lstat(dstPath) + if lerr != nil { + syncErr = burrito.WrapErrorf(lerr, osStatErrorAny, dstPath) + return + } + if linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { + if err := os.Remove(dstPath); err != nil { + syncErr = burrito.WrapErrorf(err, osRemoveError, dstPath) + return + } + } } + syncDir(srcPath, dstPath) + continue } - return CopyFile(srcPath, destPath) - } else { - Logger.Debugf("SYNC: Skipping file %s", srcPath) + + g.Go(func() error { + if ctx.Err() != nil { + return ctx.Err() + } + dstMeta := cache.get(dstPath) + if dstMeta != nil && dstMeta.IsDir() { + linfo, lerr := os.Lstat(dstPath) + if lerr != nil { + return burrito.WrapErrorf(lerr, osStatErrorAny, dstPath) + } + if linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { + if err := os.Remove(dstPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, dstPath) + } + } else { + if err := os.RemoveAll(dstPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, dstPath) + } + } + dstMeta = nil + } + needsCopy := dstMeta == nil || srcMeta == nil || + srcMeta.Size() != dstMeta.Size() || + !srcMeta.ModTime().Equal(dstMeta.ModTime()) + if !needsCopy { + return nil + } + Logger.Debugf("SYNC: Copying file %s to %s", srcPath, dstPath) + if dstMeta != nil { + if err := os.Remove(dstPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, dstPath) + } + } + if err := CopyFile(srcPath, dstPath); err != nil { + return err + } + cache.invalidate(dstPath) + if makeReadOnly { + os.Chmod(dstPath, 0444) + } + return nil + }) } - return nil - }) - if err != nil { - return burrito.WrapErrorf(err, osCopyError, source, destination) } - // A simple linked list implementation - type Node struct { - next *Node - value string + syncDir(source, destination) + waitErr := g.Wait() + if syncErr != nil { + return burrito.WrapErrorf(syncErr, osCopyError, source, destination) } - // Remove files/folders in destination that are not in source - var root = &Node{ - next: nil, - value: "", + if waitErr != nil { + return burrito.WrapErrorf(waitErr, osCopyError, source, destination) } - err = filepath.WalkDir(destination, func(destPath string, d fs.DirEntry, err error) error { - if err != nil { - return err + + // Phase 2: Cleanup destination — remove files not in source (parallel) + g2, ctx2 := errgroup.WithContext(context.Background()) + g2.SetLimit(maxWorkers) + + var cleanupErr error + var cleanupDir func(srcDir, dstDir string) + cleanupDir = func(srcDir, dstDir string) { + if cleanupErr != nil { + return } - relPath, err := filepath.Rel(destination, destPath) + entries, err := os.ReadDir(dstDir) if err != nil { - return burrito.WrapErrorf(err, filepathRelError, destination, destPath) - } - srcPath := filepath.Join(source, relPath) - if _, err := os.Stat(srcPath); os.IsNotExist(err) { - Logger.Debugf("SYNC: Removing file %s", destPath) - next := &Node{ - next: root, - value: destPath, - } - root = next + cleanupErr = burrito.WrapErrorf(err, osReadDirError, dstDir) + return } - return nil - }) - if err != nil { - return burrito.PassError(err) - } + for _, entry := range entries { + if cleanupErr != nil || ctx2.Err() != nil { + return + } + srcPath := filepath.Join(srcDir, entry.Name()) + dstPath := filepath.Join(dstDir, entry.Name()) - for root.next != nil { - err = os.RemoveAll(root.value) - if err != nil { - return burrito.WrapErrorf(err, osRemoveError, root.value) + isLink := entry.Type()&(fs.ModeSymlink|fs.ModeIrregular) != 0 + isDir := entry.IsDir() + + if cache.get(srcPath) == nil { + g2.Go(func() error { + if ctx2.Err() != nil { + return ctx2.Err() + } + Logger.Debugf("SYNC: Removing %s", dstPath) + if isLink || !isDir { + return os.Remove(dstPath) + } + return os.RemoveAll(dstPath) + }) + } else if isDir && !isLink { + cleanupDir(srcPath, dstPath) + } } - root = root.next } - // Make files read only if this option is selected + cleanupDir(source, destination) + cleanupWaitErr := g2.Wait() + if cleanupErr != nil { + return burrito.PassError(cleanupErr) + } + if cleanupWaitErr != nil { + return burrito.PassError(cleanupWaitErr) + } + if makeReadOnly { Logger.Infof("Changing the access for output path to "+ "read-only.\n\tPath: %s", destination) err := filepath.WalkDir(destination, func(s string, d fs.DirEntry, e error) error { - if e != nil { - // Error message isn't important as it's not passed further - // in the code return e } if !d.IsDir() { @@ -962,8 +1104,7 @@ func SyncDirectories( if err != nil { Logger.Warnf( "Failed to change access of the output path to read-only.\n"+ - "\tPath: %s", - destination) + "\tPath: %s", destination) } } return nil diff --git a/regolith/profile.go b/regolith/profile.go index fd2a523c..bef4d1ac 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -3,6 +3,7 @@ package regolith import ( "encoding/json" "fmt" + "io/fs" "os" "os/exec" "path/filepath" @@ -167,12 +168,31 @@ func SetupTmpFiles(context RunContext) error { // Clean the temporary directory isRegularRun := !useSizeTimeCheck && !useSymlinkExport - if isRegularRun || shouldCreateSymlinks { + if isRegularRun { Logger.Debugf("Cleaning \"%s\"", absTmpPath) err := os.RemoveAll(absTmpPath) if err != nil { return burrito.WrapErrorf(err, osRemoveError, absTmpPath) } + } else if shouldCreateSymlinks { + for _, tmpPath := range []string{bpTmpPath, rpTmpPath} { + linfo, err := os.Lstat(tmpPath) + if err != nil { + if os.IsNotExist(err) { + continue + } + return burrito.WrapErrorf(err, osStatErrorAny, tmpPath) + } + if linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { + if err := os.Remove(tmpPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, tmpPath) + } + } else { + if err := os.RemoveAll(tmpPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, tmpPath) + } + } + } } // Prepare temp path root @@ -183,25 +203,23 @@ func SetupTmpFiles(context RunContext) error { // Create symlinks if shouldCreateSymlinks { - // Check deletion safety - editedFiles := LoadEditedFiles(dotRegolithPath) - err := editedFiles.CheckDeletionSafety(rpExportPath, bpExportPath) - if err != nil { - return burrito.WrapErrorf( - err, - checkDeletionSafetyError, - rpExportPath, bpExportPath) + if !UnsafeMode { + editedFiles := LoadEditedFiles(dotRegolithPath) + err := editedFiles.CheckDeletionSafety(rpExportPath, bpExportPath) + if err != nil { + return burrito.WrapErrorf( + err, + checkDeletionSafetyError, + rpExportPath, bpExportPath) + } } - - // Remove existing exported paths - if err := os.RemoveAll(bpExportPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, bpExportPath) + if err := os.MkdirAll(bpExportPath, 0755); err != nil { + return burrito.WrapErrorf(err, osMkdirError, bpExportPath) } - if err := os.RemoveAll(rpExportPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, rpExportPath) + if err := os.MkdirAll(rpExportPath, 0755); err != nil { + return burrito.WrapErrorf(err, osMkdirError, rpExportPath) } - // Create symlinks if err := createDirLink(filepath.Join(absTmpPath, "BP"), bpExportPath); err != nil { return burrito.WrapErrorf(err, createDirLinkError, filepath.Join(absTmpPath, "BP"), bpExportPath) } @@ -292,9 +310,7 @@ func SetupTmpFiles(context RunContext) error { editedFiles := NewEditedFiles() err = editedFiles.UpdateFromPaths(rpExportPath, bpExportPath) if err != nil { - return burrito.WrapError( - err, - "Failed to create a list of files safe to edit") + return burrito.WrapError(err, updatedFilesUpdateError) } err = editedFiles.Dump(dotRegolithPath) if err != nil { From f197c57ffc710c573f35279cc8382c9a6e2dae2a Mon Sep 17 00:00:00 2001 From: FrederoxDev Date: Mon, 27 Apr 2026 13:02:36 +0100 Subject: [PATCH 13/31] fix isSymlinkTo --- regolith/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regolith/utils.go b/regolith/utils.go index b3b4e5a1..b4c9d941 100644 --- a/regolith/utils.go +++ b/regolith/utils.go @@ -494,7 +494,7 @@ func SliceAny[T any](slice []T, predicate func(T) bool) bool { func isSymlinkTo(path, target string) bool { info, err := os.Lstat(path) - if err != nil || info.Mode()&os.ModeSymlink == 0 { + if err != nil || info.Mode()&(os.ModeSymlink|os.ModeIrregular) == 0 { return false } dest, err := os.Readlink(path) @@ -511,7 +511,7 @@ func isSymlinkTo(path, target string) bool { func isSymlink(path string) bool { info, err := os.Lstat(path) - if err != nil || info.Mode()&os.ModeSymlink == 0 { + if err != nil || info.Mode()&(os.ModeSymlink|os.ModeIrregular) == 0 { return false } return true From 375cf4b1dd2f2ae568adcc6b78dce73b88bd93a2 Mon Sep 17 00:00:00 2001 From: FrederoxDev Date: Mon, 27 Apr 2026 14:16:25 +0100 Subject: [PATCH 14/31] fix: syncLink --- regolith/file_system.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/regolith/file_system.go b/regolith/file_system.go index db3a6676..80a3c66d 100644 --- a/regolith/file_system.go +++ b/regolith/file_system.go @@ -887,8 +887,18 @@ func syncLink(srcPath, dstPath string) error { Logger.Debugf("SYNC: Skipping irregular file %s", srcPath) return nil } - if err := os.Remove(dstPath); err != nil && !os.IsNotExist(err) { - return burrito.WrapErrorf(err, osRemoveError, dstPath) + if linfo, lerr := os.Lstat(dstPath); lerr == nil { + if linfo.IsDir() && linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) == 0 { + if err := os.RemoveAll(dstPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, dstPath) + } + } else { + if err := os.Remove(dstPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, dstPath) + } + } + } else if !os.IsNotExist(lerr) { + return burrito.WrapErrorf(lerr, osStatErrorAny, dstPath) } targetInfo, err := os.Stat(srcPath) if err == nil && targetInfo.IsDir() { @@ -964,6 +974,7 @@ func SyncDirectories( syncErr = burrito.WrapErrorf(err, osRemoveError, dstPath) return } + cache.invalidate(dstPath) } if dstMeta != nil && dstMeta.IsDir() { linfo, lerr := os.Lstat(dstPath) @@ -976,6 +987,7 @@ func SyncDirectories( syncErr = burrito.WrapErrorf(err, osRemoveError, dstPath) return } + cache.invalidate(dstPath) } } syncDir(srcPath, dstPath) From 806cbf96d980294b94213516b505245592f49c75 Mon Sep 17 00:00:00 2001 From: FrederoxDev Date: Mon, 27 Apr 2026 15:50:13 +0100 Subject: [PATCH 15/31] junctions fixes --- regolith/export.go | 6 +-- regolith/file_protection.go | 6 +++ regolith/file_system.go | 87 +++++++++++++++++++++++-------------- regolith/profile.go | 18 +------- 4 files changed, 64 insertions(+), 53 deletions(-) diff --git a/regolith/export.go b/regolith/export.go index 74193cc4..a5a07a6f 100644 --- a/regolith/export.go +++ b/regolith/export.go @@ -312,14 +312,12 @@ func exportProjectRpAndBp(profile Profile, rpPath, bpPath string, ctx RunContext if !IsExperimentEnabled(SizeTimeCheck) { // Clearing output locations MeasureStart("Export - Clean") - err = os.RemoveAll(bpPath) - if err != nil { + if err := removeJunctionSafe(bpPath); err != nil { return burrito.WrapErrorf( err, "Failed to clear behavior pack from build path %q.\n"+ "Are user permissions correct?", bpPath) } - err = os.RemoveAll(rpPath) - if err != nil { + if err := removeJunctionSafe(rpPath); err != nil { return burrito.WrapErrorf( err, "Failed to clear resource pack from build path %q.\n"+ "Are user permissions correct?", rpPath) diff --git a/regolith/file_protection.go b/regolith/file_protection.go index 6b816298..a26f9563 100644 --- a/regolith/file_protection.go +++ b/regolith/file_protection.go @@ -109,6 +109,12 @@ func NewEditedFiles() EditedFiles { // listFiles returns a slice of strings with paths to all the files // starting from "path" func listFiles(path string) ([]string, error) { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return make([]string, 0), nil + } + return nil, burrito.WrapErrorf(err, osStatErrorAny, path) + } // 150 is just an arbitrary number I chose to avoid constant memory // allocation while expanding the slice capacity result := make([]string, 0, 150) diff --git a/regolith/file_system.go b/regolith/file_system.go index 80a3c66d..70254094 100644 --- a/regolith/file_system.go +++ b/regolith/file_system.go @@ -881,27 +881,41 @@ func (c *syncMetadataCache) invalidate(path string) { c.m.Delete(path) } +// removeJunctionSafe removes a path that might be a junction/symlink. +// For junctions/symlinks it uses os.Remove (won't follow into target). +// For real directories it uses os.RemoveAll. +// Returns nil if the path doesn't exist. +func removeJunctionSafe(path string) error { + linfo, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return burrito.WrapErrorf(err, osStatErrorAny, path) + } + if linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { + return os.Remove(path) + } + if linfo.IsDir() { + return os.RemoveAll(path) + } + return os.Remove(path) +} + func syncLink(srcPath, dstPath string) error { linkTarget, err := os.Readlink(srcPath) if err != nil { Logger.Debugf("SYNC: Skipping irregular file %s", srcPath) return nil } - if linfo, lerr := os.Lstat(dstPath); lerr == nil { - if linfo.IsDir() && linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) == 0 { - if err := os.RemoveAll(dstPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, dstPath) - } - } else { - if err := os.Remove(dstPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, dstPath) - } - } - } else if !os.IsNotExist(lerr) { - return burrito.WrapErrorf(lerr, osStatErrorAny, dstPath) + if err := removeJunctionSafe(dstPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, dstPath) } - targetInfo, err := os.Stat(srcPath) - if err == nil && targetInfo.IsDir() { + srcLinfo, lerr := os.Lstat(srcPath) + isJunction := lerr == nil && srcLinfo.Mode()&fs.ModeIrregular != 0 + targetInfo, serr := os.Stat(srcPath) + isDirLink := isJunction || (serr == nil && targetInfo.IsDir()) + if isDirLink { err = createDirLink(dstPath, linkTarget) } else { err = os.Symlink(linkTarget, dstPath) @@ -937,6 +951,10 @@ func SyncDirectories( return } if cache.get(dstDir) == nil { + if err := removeJunctionSafe(dstDir); err != nil { + syncErr = burrito.WrapErrorf(err, osRemoveError, dstDir) + return + } if err := os.MkdirAll(dstDir, 0755); err != nil { syncErr = burrito.WrapErrorf(err, osMkdirError, dstDir) return @@ -970,13 +988,15 @@ func SyncDirectories( if srcMeta != nil && srcMeta.IsDir() { dstMeta := cache.get(dstPath) if dstMeta != nil && !dstMeta.IsDir() { - if err := os.Remove(dstPath); err != nil { + // Destination is a file but source is a dir — remove it + if err := removeJunctionSafe(dstPath); err != nil { syncErr = burrito.WrapErrorf(err, osRemoveError, dstPath) return } cache.invalidate(dstPath) - } - if dstMeta != nil && dstMeta.IsDir() { + } else if dstMeta != nil { + // Destination looks like a dir via os.Stat — check if it's + // actually a junction and remove just the junction linfo, lerr := os.Lstat(dstPath) if lerr != nil { syncErr = burrito.WrapErrorf(lerr, osStatErrorAny, dstPath) @@ -1000,18 +1020,8 @@ func SyncDirectories( } dstMeta := cache.get(dstPath) if dstMeta != nil && dstMeta.IsDir() { - linfo, lerr := os.Lstat(dstPath) - if lerr != nil { - return burrito.WrapErrorf(lerr, osStatErrorAny, dstPath) - } - if linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { - if err := os.Remove(dstPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, dstPath) - } - } else { - if err := os.RemoveAll(dstPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, dstPath) - } + if err := removeJunctionSafe(dstPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, dstPath) } dstMeta = nil } @@ -1039,6 +1049,20 @@ func SyncDirectories( } } + // If the destination is a junction (from symlink_export), ensure its target + // directory exists so os.Stat follows it successfully and we don't replace + // the junction with a real directory. + if linfo, lerr := os.Lstat(destination); lerr == nil { + if linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { + if target, err := os.Readlink(destination); err == nil { + if !filepath.IsAbs(target) { + target = filepath.Join(filepath.Dir(destination), target) + } + os.MkdirAll(target, 0755) + } + } + } + syncDir(source, destination) waitErr := g.Wait() if syncErr != nil { @@ -1080,10 +1104,7 @@ func SyncDirectories( return ctx2.Err() } Logger.Debugf("SYNC: Removing %s", dstPath) - if isLink || !isDir { - return os.Remove(dstPath) - } - return os.RemoveAll(dstPath) + return removeJunctionSafe(dstPath) }) } else if isDir && !isLink { cleanupDir(srcPath, dstPath) diff --git a/regolith/profile.go b/regolith/profile.go index bef4d1ac..e33e7af8 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -3,7 +3,6 @@ package regolith import ( "encoding/json" "fmt" - "io/fs" "os" "os/exec" "path/filepath" @@ -176,21 +175,8 @@ func SetupTmpFiles(context RunContext) error { } } else if shouldCreateSymlinks { for _, tmpPath := range []string{bpTmpPath, rpTmpPath} { - linfo, err := os.Lstat(tmpPath) - if err != nil { - if os.IsNotExist(err) { - continue - } - return burrito.WrapErrorf(err, osStatErrorAny, tmpPath) - } - if linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { - if err := os.Remove(tmpPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, tmpPath) - } - } else { - if err := os.RemoveAll(tmpPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, tmpPath) - } + if err := removeJunctionSafe(tmpPath); err != nil { + return burrito.WrapErrorf(err, osRemoveError, tmpPath) } } } From b39d58126a4a54419dcd522162e6b56740d16738 Mon Sep 17 00:00:00 2001 From: Hydrogen <96733109+dev-hydrogen@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:19:02 +0300 Subject: [PATCH 16/31] multiple export targets --- regolith/config.go | 68 ++++++ regolith/export.go | 150 ++++++++---- regolith/file_system.go | 46 ++-- regolith/main_functions.go | 10 +- regolith/profile.go | 80 +++++-- test/common.go | 31 ++- test/export_targets_test.go | 444 ++++++++++++++++++++++++++++++++++++ 7 files changed, 734 insertions(+), 95 deletions(-) create mode 100644 test/export_targets_test.go diff --git a/regolith/config.go b/regolith/config.go index 9be0b6cd..706b09f2 100644 --- a/regolith/config.go +++ b/regolith/config.go @@ -1,6 +1,7 @@ package regolith import ( + "encoding/json" "fmt" "github.com/Bedrock-OSS/go-burrito/burrito" @@ -37,6 +38,37 @@ type ExportTarget struct { Build string `json:"build,omitempty"` // The type of Minecraft build for the 'develop' } +// ExportTargets is the config representation of a profile's "export" value. +// It accepts both the single-object form and the multi-target array +// form. When marshaling, a single target is written as an object to keep newly +// generated configs backward compatible with older Regolith versions. +type ExportTargets []ExportTarget + +// IsZero lets json:",omitzero" omit an unset target list. +func (et ExportTargets) IsZero() bool { + return len(et) == 0 +} + +func (et ExportTargets) MarshalJSON() ([]byte, error) { + if len(et) == 1 { + return json.Marshal(et[0]) + } + return json.Marshal([]ExportTarget(et)) +} + +func (et *ExportTargets) UnmarshalJSON(data []byte) error { + var raw any + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + targets, err := ExportTargetsFromObject(raw) + if err != nil { + return err + } + *et = targets + return nil +} + // Packs is a part of "config.json" that points to the source behavior and // resource packs. type Packs struct { @@ -207,6 +239,42 @@ func RegolithProjectFromObject( return result, nil } +// ExportTargetsFromObject parses the "export" value which can be either a +// single object (backward compatible) or an array of objects. +func ExportTargetsFromObject(exportValue any) (ExportTargets, error) { + switch v := exportValue.(type) { + case map[string]any: + et, err := ExportTargetFromObject(v) + if err != nil { + return nil, burrito.WrapErrorf(err, jsonPropertyParseError, "export") + } + return ExportTargets{et}, nil + case []any: + if len(v) == 0 { + return nil, burrito.WrappedErrorf( + "The \"export\" array must contain at least one entry") + } + targets := make(ExportTargets, 0, len(v)) + for i, item := range v { + obj, ok := item.(map[string]any) + if !ok { + return nil, burrito.WrappedErrorf( + jsonPathTypeError, fmt.Sprintf("export->%d", i), "object") + } + et, err := ExportTargetFromObject(obj) + if err != nil { + return nil, burrito.WrapErrorf( + err, jsonPropertyParseError, fmt.Sprintf("export->%d", i)) + } + targets = append(targets, et) + } + return targets, nil + default: + return nil, burrito.WrappedErrorf( + jsonPropertyTypeError, "export", "object or array") + } +} + // ExportTargetFromObject creates a "ExportTarget" object from // map[string]interface{} func ExportTargetFromObject(obj map[string]any) (ExportTarget, error) { diff --git a/regolith/export.go b/regolith/export.go index b093126a..df38188c 100644 --- a/regolith/export.go +++ b/regolith/export.go @@ -3,10 +3,12 @@ package regolith import ( "os" "path/filepath" + "runtime" "strings" "sync" "github.com/Bedrock-OSS/go-burrito/burrito" + "github.com/otiai10/copy" "golang.org/x/mod/semver" ) @@ -224,67 +226,88 @@ func GetExportNames(exportTarget ExportTarget, ctx RunContext) (bpName string, r return } +type resolvedExportTarget struct { + target ExportTarget + bpPath string + rpPath string +} + // ExportProject copies files from the tmp paths (tmp/BP and tmp/RP) into -// the project's export target. The paths are generated with GetExportPaths. +// the project's export targets. The paths are generated with GetExportPaths. func ExportProject(ctx RunContext) error { MeasureStart("Export - GetExportPaths") profile, err := ctx.GetProfile() if err != nil { return burrito.WrapError(err, runContextGetProfileError) } - if profile.ExportTarget.Target == "none" { - Logger.Debugf("Export target is set to \"none\". Skipping export.") + // Resolve all non-"none" targets before modifying any export path. This + // keeps failure atomic when a later target has an invalid path or unsafe + // existing files. + var activeTargets []resolvedExportTarget + for _, exportTarget := range profile.activeExportTargets() { + bpPath, rpPath, err := GetExportPaths(exportTarget, ctx) + if err != nil { + return burrito.WrapError(err, getExportPathsError) + } + activeTargets = append(activeTargets, resolvedExportTarget{ + target: exportTarget, + bpPath: bpPath, + rpPath: rpPath, + }) + } + if len(activeTargets) == 0 { + Logger.Debugf("All export targets are set to \"none\". Skipping export.") return nil } - // Get the necessary paths and variables dotRegolithPath := ctx.DotRegolithPath - exportTarget := profile.ExportTarget - bpPath, rpPath, err := GetExportPaths(exportTarget, ctx) - if err != nil { - return burrito.WrapError( - err, getExportPathsError) - } - // Load edited files - MeasureStart("Export - CheckDeletionSafety") + useSymlink := IsExperimentEnabled(SymlinkExport) && len(activeTargets) == 1 editedFiles := LoadEditedFiles(dotRegolithPath) - err = editedFiles.CheckDeletionSafety(rpPath, bpPath) - if err != nil { - return burrito.WrapErrorf( - err, - checkDeletionSafetyError, - rpPath, bpPath) - } - // Export RP and BP if necessary - if IsExperimentEnabled(SymlinkExport) { - Logger.Debugf("SymlinkExport experiment is enabled. Skipping RP and BP export.") - } else { - err = exportProjectRpAndBp(profile, rpPath, bpPath, ctx) + + for _, exportTarget := range activeTargets { + MeasureStart("Export - CheckDeletionSafety") + err = editedFiles.CheckDeletionSafety(exportTarget.rpPath, exportTarget.bpPath) if err != nil { - return burrito.PassError(err) + return burrito.WrapErrorf( + err, checkDeletionSafetyError, exportTarget.rpPath, exportTarget.bpPath) } } - // Export data for exportData filters - MeasureStart("Export - ExportData") - err = exportProjectData(profile, ctx) - if err != nil { - return burrito.PassError(err) + + for i, exportTarget := range activeTargets { + // Symlink export already placed files for the only active target. + if useSymlink && i == 0 { + Logger.Debugf("SymlinkExport experiment is enabled. Skipping RP and BP export.") + } else { + // Move is only safe when there is exactly one active target + // and symlink export is off, since tmp/ is the sole source and + // moving from a symlinked tmp would destroy the first target. + canMove := len(activeTargets) == 1 && !useSymlink + err = exportProjectRpAndBp( + exportTarget.target, exportTarget.rpPath, exportTarget.bpPath, + ctx, canMove) + if err != nil { + return burrito.PassError(err) + } + } } MeasureStart("Export - EditedFiles.UpdateFromPaths") - // Update or create edited_files.json - err = editedFiles.UpdateFromPaths(rpPath, bpPath) - if err != nil { - return burrito.WrapError( - err, - "Failed to create a list of files edited by this 'regolith run'") + for _, exportTarget := range activeTargets { + err = editedFiles.UpdateFromPaths(exportTarget.rpPath, exportTarget.bpPath) + if err != nil { + return burrito.WrapError( + err, + "Failed to create a list of files edited by this 'regolith run'") + } } err = editedFiles.Dump(dotRegolithPath) if err != nil { return burrito.WrapError(err, updatedFilesDumpError) } - // Remove the exported pack paths if they're empty - if !IsExperimentEnabled(SymlinkExport) { - MeasureStart("Export - Remove Empty Export Paths") - for _, packPath := range []string{rpPath, bpPath} { + MeasureStart("Export - Remove Empty Export Paths") + for i, exportTarget := range activeTargets { + if useSymlink && i == 0 { + continue + } + for _, packPath := range []string{exportTarget.rpPath, exportTarget.bpPath} { pathEmpty, _ := IsDirEmpty(packPath) if pathEmpty { if err := os.Remove(packPath); err != nil { @@ -296,22 +319,25 @@ func ExportProject(ctx RunContext) error { } } } + // Export data once (not per target) + MeasureStart("Export - ExportData") + err = exportProjectData(profile, ctx) + if err != nil { + return burrito.PassError(err) + } MeasureEnd() return nil } -// exportProjectRpAndBp is a helper function for ExportProject. It exports the 'rp' -// and 'bp' folders to the target location. This assumes that the symlinkExport -// is disabled. -func exportProjectRpAndBp(profile Profile, rpPath, bpPath string, ctx RunContext) error { +// exportProjectRpAndBp is a helper function for ExportProject. It exports the +// 'rp' and 'bp' folders to the target location. Moving is only safe for a +// single active target without symlink export, since the tmp source must remain +// intact for additional targets. +func exportProjectRpAndBp(exportTarget ExportTarget, rpPath, bpPath string, ctx RunContext, allowMove bool) error { dotRegolithPath := ctx.DotRegolithPath - exportTarget := profile.ExportTarget var err error - // When comparing the size and modification time of the files, we need to - // keep the files in target paths. if !IsExperimentEnabled(SizeTimeCheck) { - // Clearing output locations MeasureStart("Export - Clean") err = os.RemoveAll(bpPath) if err != nil { @@ -348,8 +374,10 @@ func exportProjectRpAndBp(profile Profile, rpPath, bpPath string, ctx RunContext var e error if IsExperimentEnabled(SizeTimeCheck) { e = SyncDirectories(filepath.Join(absWorkingDir, subpathInTmp), packPath, exportTarget.ReadOnly) - } else { + } else if allowMove { e = MoveOrCopy(filepath.Join(absWorkingDir, subpathInTmp), packPath, exportTarget.ReadOnly, true) + } else { + e = copyExportPath(filepath.Join(absWorkingDir, subpathInTmp), packPath, exportTarget.ReadOnly) } if e != nil { errChan <- burrito.WrapErrorf(e, "Failed to export %s pack.", packType) @@ -369,6 +397,27 @@ func exportProjectRpAndBp(profile Profile, rpPath, bpPath string, ctx RunContext return nil } +func copyExportPath(source, destination string, makeReadOnly bool) error { + copySource := source + if resolvedSource, err := filepath.EvalSymlinks(source); err == nil { + copySource = resolvedSource + } + copyOptions := copy.Options{ + PreserveTimes: false, + Sync: false, + } + if runtime.GOOS == "windows" { + copyOptions.PermissionControl = copy.DoNothing + } + if err := copy.Copy(copySource, destination, copyOptions); err != nil { + return burrito.WrapErrorf(err, osCopyError, source, destination) + } + if makeReadOnly { + setPathReadOnly(destination) + } + return nil +} + // exportProjectData is a helper function for ExportProject. It exports the 'data' // folder back to the project's source files for the filters that opted-in for // that with exportProjectData option. @@ -405,6 +454,9 @@ func exportProjectData(profile Profile, ctx RunContext) error { if err != nil { return burrito.WrapError(err, "Failed to walk the list of the filters.") } + if len(exportedFilterNames) == 0 { + return nil + } // The root of the data path cannot be deleted because the // "regolith watch" function would stop watching the file changes // (due to Windows API limitation). diff --git a/regolith/file_system.go b/regolith/file_system.go index 054b8a00..67afc253 100644 --- a/regolith/file_system.go +++ b/regolith/file_system.go @@ -821,33 +821,35 @@ func MoveOrCopy( return err } } - // Make files read only if this option is selected if makeReadOnly { - Logger.Infof("Changing the access for output path to "+ - "read-only.\n\tPath: %s", destination) - err := filepath.WalkDir(destination, - func(s string, d fs.DirEntry, e error) error { - - if e != nil { - // Error message isn't important as it's not passed further - // in the code - return e - } - if !d.IsDir() { - os.Chmod(s, 0444) - } - return nil - }) - if err != nil { - Logger.Warnf( - "Failed to change access of the output path to read-only.\n"+ - "\tPath: %s", - destination) - } + setPathReadOnly(destination) } return nil } +func setPathReadOnly(path string) { + Logger.Infof("Changing the access for output path to "+ + "read-only.\n\tPath: %s", path) + err := filepath.WalkDir(path, + func(s string, d fs.DirEntry, e error) error { + if e != nil { + // Error message isn't important as it's not passed further + // in the code. + return e + } + if !d.IsDir() { + os.Chmod(s, 0444) + } + return nil + }) + if err != nil { + Logger.Warnf( + "Failed to change access of the output path to read-only.\n"+ + "\tPath: %s", + path) + } +} + // SyncDirectories copies the source to destination while checking size and modification time. // If the file in the destination is different than the one in the source, it's overwritten, // otherwise it's skipped (the destination file is not modified). diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 59d777f5..29974de4 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -487,10 +487,12 @@ func Init(debug, force bool, env string) error { FilterCollection: FilterCollection{ Filters: []FilterRunner{}, }, - ExportTarget: ExportTarget{ - Target: "development", - Build: "standard", - ReadOnly: false, + ExportTargets: ExportTargets{ + { + Target: "development", + Build: "standard", + ReadOnly: false, + }, }, }, }, diff --git a/regolith/profile.go b/regolith/profile.go index fd2a523c..540c2be1 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -136,16 +136,20 @@ func SetupTmpFiles(context RunContext) error { if err != nil { return burrito.WrapErrorf(err, runContextGetProfileError) } - bpExportPath, rpExportPath, err = GetExportPaths(profile.ExportTarget, context) - if err != nil { - return burrito.WrapError(err, getExportPathsError) - } - if profile.ExportTarget.Target == "none" { + activeTargets := profile.activeExportTargets() + if len(activeTargets) != 1 { + if len(activeTargets) > 1 { + Logger.Debugf("SymlinkExport experiment is enabled but the profile has multiple active export targets. Using regular export.") + } useSymlinkExport = false } else { + primaryTarget := activeTargets[0] + bpExportPath, rpExportPath, err = GetExportPaths(primaryTarget, context) + if err != nil { + return burrito.WrapError(err, getExportPathsError) + } bpLink := isSymlinkTo(bpTmpPath, bpExportPath) rpLink := isSymlinkTo(rpTmpPath, rpExportPath) - // If either symlink doesn't exist, create them shouldCreateSymlinks = !bpLink || !rpLink } } @@ -552,15 +556,55 @@ func (sc *ShellCommands) GetCommandsForCurrentOS() []string { } } -// Profile is a collection of filters and an export target -// When editing, adjust ProfileFromObject function as well +// Profile is a collection of filters and export targets. +// When editing, adjust ProfileFromObject function as well. type Profile struct { FilterCollection - ExportTarget ExportTarget `json:"export,omitzero"` + ExportTargets ExportTargets `json:"export,omitzero"` + // Deprecated: use ExportTargets. This field is kept as a compatibility + // fallback for Go callers that still build Profile values with one export + // target. + ExportTarget ExportTarget `json:"-"` PreShell ShellCommands `json:"preShell,omitzero"` PostShell ShellCommands `json:"postShell,omitzero"` } +func (p Profile) exportTargets() ExportTargets { + if len(p.ExportTargets) > 0 { + return p.ExportTargets + } + if p.ExportTarget.Target != "" { + return ExportTargets{p.ExportTarget} + } + return nil +} + +func (p Profile) activeExportTargets() ExportTargets { + targets := p.exportTargets() + activeTargets := make(ExportTargets, 0, len(targets)) + for _, target := range targets { + if target.Target != "none" { + activeTargets = append(activeTargets, target) + } + } + return activeTargets +} + +func (p Profile) MarshalJSON() ([]byte, error) { + type profileJSON struct { + Filters []FilterRunner `json:"filters"` + ExportTargets ExportTargets `json:"export,omitzero"` + PreShell ShellCommands `json:"preShell,omitzero"` + PostShell ShellCommands `json:"postShell,omitzero"` + } + return json.Marshal(profileJSON{ + Filters: p.Filters, + ExportTargets: p.exportTargets(), + PreShell: p.PreShell, + PostShell: p.PostShell, + }) +} + func shellCommandsFromObject(obj map[string]any, key string) (ShellCommands, error) { var result ShellCommands if shellObj, ok := obj[key]; ok { @@ -655,19 +699,19 @@ func ProfileFromObject( } result.Filters = append(result.Filters, filterRunner) } - // ExportTarget - if _, ok := obj["export"]; !ok { - return result, burrito.WrappedErrorf(jsonPathMissingError, "export") - } - export, ok := obj["export"].(map[string]any) + // ExportTargets + exportValue, ok := obj["export"] if !ok { - return result, burrito.WrappedErrorf(jsonPathTypeError, "export", "object") + return result, burrito.WrappedErrorf(jsonPathMissingError, "export") } - exportTarget, err := ExportTargetFromObject(export) + exportTargets, err := ExportTargetsFromObject(exportValue) if err != nil { - return result, burrito.WrapErrorf(err, jsonPathParseError, "export") + return result, burrito.PassError(err) + } + result.ExportTargets = exportTargets + if len(exportTargets) > 0 { + result.ExportTarget = exportTargets[0] } - result.ExportTarget = exportTarget // PreShell and PostShell preShell, err := shellCommandsFromObject(obj, "preShell") if err != nil { diff --git a/test/common.go b/test/common.go index 19ae158c..2fe5ff12 100644 --- a/test/common.go +++ b/test/common.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/otiai10/copy" ) @@ -225,7 +226,7 @@ func prepareTestDirectory(path string, t *testing.T) string { const testResultsDir = "test_results" // Create the output directory result := filepath.Join(testResultsDir, path) - if err := os.RemoveAll(result); err != nil { + if err := removeAllForTest(result); err != nil { t.Fatalf( "Failed to delete the files form the testing directory."+ "\nPath: %q\nError: %v", @@ -247,6 +248,28 @@ func prepareTestDirectory(path string, t *testing.T) string { return result } +func removeAllForTest(path string) error { + var err error + for range 3 { + _ = filepath.WalkDir(path, func(currPath string, entry fs.DirEntry, walkErr error) error { + if walkErr == nil { + mode := os.FileMode(0666) + if entry.IsDir() { + mode = 0777 + } + _ = os.Chmod(currPath, mode) + } + return nil + }) + err = os.RemoveAll(path) + if err == nil { + return nil + } + time.Sleep(100 * time.Millisecond) + } + return err +} + // getWdOrFatal returns the current working directory or exits with t.Fatal in // case of error. func getWdOrFatal(t *testing.T) string { @@ -262,7 +285,11 @@ func getWdOrFatal(t *testing.T) string { func copyFilesOrFatal(src, dest string, t *testing.T) { os.MkdirAll(dest, 0755) err := copy.Copy( - src, dest, copy.Options{PreserveTimes: false, Sync: false}) + src, dest, copy.Options{ + PreserveTimes: false, + Sync: false, + PermissionControl: copy.DoNothing, + }) if err != nil { t.Fatalf( "Failed to copy files.\nSource: %s\nDestination: %s\nError: %v", diff --git a/test/export_targets_test.go b/test/export_targets_test.go new file mode 100644 index 00000000..08ee093b --- /dev/null +++ b/test/export_targets_test.go @@ -0,0 +1,444 @@ +package test + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/Bedrock-OSS/regolith/regolith" +) + +func TestExportTargetsFromObject_SingleObject(t *testing.T) { + input := map[string]any{ + "target": "development", + "readOnly": false, + } + targets, err := regolith.ExportTargetsFromObject(input) + if err != nil { + t.Fatal(err) + } + if len(targets) != 1 { + t.Fatalf("Expected 1 target, got %d", len(targets)) + } + if targets[0].Target != "development" { + t.Fatalf("Expected target \"development\", got %q", targets[0].Target) + } +} + +func TestExportTargetsFromObject_Array(t *testing.T) { + input := []any{ + map[string]any{"target": "development", "build": "standard"}, + map[string]any{"target": "local"}, + } + targets, err := regolith.ExportTargetsFromObject(input) + if err != nil { + t.Fatal(err) + } + if len(targets) != 2 { + t.Fatalf("Expected 2 targets, got %d", len(targets)) + } + if targets[0].Target != "development" { + t.Fatalf("Expected first target \"development\", got %q", targets[0].Target) + } + if targets[0].Build != "standard" { + t.Fatalf("Expected first target build \"standard\", got %q", targets[0].Build) + } + if targets[1].Target != "local" { + t.Fatalf("Expected second target \"local\", got %q", targets[1].Target) + } +} + +func TestExportTargetsFromObject_EmptyArray(t *testing.T) { + input := []any{} + _, err := regolith.ExportTargetsFromObject(input) + if err == nil { + t.Fatal("Expected error for empty array, got nil") + } +} + +func TestExportTargetsFromObject_InvalidType(t *testing.T) { + _, err := regolith.ExportTargetsFromObject("invalid") + if err == nil { + t.Fatal("Expected error for invalid type, got nil") + } +} + +func TestExportTargets_MarshalJSON_Single(t *testing.T) { + targets := regolith.ExportTargets{ + {Target: "development", ReadOnly: false}, + } + data, err := json.Marshal(targets) + if err != nil { + t.Fatal(err) + } + // Single target should marshal as an object, not an array + var obj map[string]any + if err := json.Unmarshal(data, &obj); err != nil { + t.Fatalf("Single target should marshal as object, got: %s", string(data)) + } + if obj["target"] != "development" { + t.Fatalf("Expected target \"development\", got %v", obj["target"]) + } +} + +func TestExportTargets_MarshalJSON_Multiple(t *testing.T) { + targets := regolith.ExportTargets{ + {Target: "development"}, + {Target: "local"}, + } + data, err := json.Marshal(targets) + if err != nil { + t.Fatal(err) + } + // Multiple targets should marshal as an array + var arr []map[string]any + if err := json.Unmarshal(data, &arr); err != nil { + t.Fatalf("Multiple targets should marshal as array, got: %s", string(data)) + } + if len(arr) != 2 { + t.Fatalf("Expected 2 elements, got %d", len(arr)) + } +} + +func TestExportTargets_UnmarshalJSON_Single(t *testing.T) { + data := []byte(`{"target": "development", "readOnly": true}`) + var targets regolith.ExportTargets + if err := json.Unmarshal(data, &targets); err != nil { + t.Fatal(err) + } + if len(targets) != 1 { + t.Fatalf("Expected 1 target, got %d", len(targets)) + } + if targets[0].Target != "development" { + t.Fatalf("Expected \"development\", got %q", targets[0].Target) + } + if !targets[0].ReadOnly { + t.Fatal("Expected ReadOnly to be true") + } +} + +func TestExportTargets_UnmarshalJSON_Array(t *testing.T) { + data := []byte(`[{"target": "development"}, {"target": "exact", "bpPath": "./build/BP", "rpPath": "./build/RP"}]`) + var targets regolith.ExportTargets + if err := json.Unmarshal(data, &targets); err != nil { + t.Fatal(err) + } + if len(targets) != 2 { + t.Fatalf("Expected 2 targets, got %d", len(targets)) + } + if targets[1].BpPath != "./build/BP" { + t.Fatalf("Expected bpPath \"./build/BP\", got %q", targets[1].BpPath) + } +} + +func TestExportTargets_UnmarshalJSON_InvalidObject(t *testing.T) { + data := []byte(`{"readOnly": true}`) + var targets regolith.ExportTargets + if err := json.Unmarshal(data, &targets); err == nil { + t.Fatal("Expected error for export target without target property") + } +} + +func TestExportTargets_UnmarshalJSON_EmptyArray(t *testing.T) { + data := []byte(`[]`) + var targets regolith.ExportTargets + if err := json.Unmarshal(data, &targets); err == nil { + t.Fatal("Expected error for empty export target array") + } +} + +func TestExportTargets_RoundTrip(t *testing.T) { + original := regolith.ExportTargets{ + {Target: "development", Build: "standard", ReadOnly: true}, + {Target: "exact", BpPath: "./bp", RpPath: "./rp"}, + } + data, err := json.Marshal(original) + if err != nil { + t.Fatal(err) + } + var decoded regolith.ExportTargets + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + if len(decoded) != len(original) { + t.Fatalf("Expected %d targets, got %d", len(original), len(decoded)) + } + for i := range original { + if decoded[i].Target != original[i].Target { + t.Fatalf("Target %d: expected %q, got %q", i, original[i].Target, decoded[i].Target) + } + } +} + +func TestProfileFromObject_SingleExportSetsCompatibilityField(t *testing.T) { + profile, err := regolith.ProfileFromObject( + map[string]any{ + "filters": []any{}, + "export": map[string]any{ + "target": "local", + }, + }, + map[string]regolith.FilterInstaller{}, + ) + if err != nil { + t.Fatal(err) + } + if len(profile.ExportTargets) != 1 { + t.Fatalf("Expected 1 target, got %d", len(profile.ExportTargets)) + } + if profile.ExportTargets[0].Target != "local" { + t.Fatalf("Expected ExportTargets[0] to be local, got %q", profile.ExportTargets[0].Target) + } + if profile.ExportTarget.Target != "local" { + t.Fatalf("Expected deprecated ExportTarget fallback to be local, got %q", profile.ExportTarget.Target) + } +} + +func TestProfileMarshalJSON_DeprecatedExportTargetFallback(t *testing.T) { + profile := regolith.Profile{ + FilterCollection: regolith.FilterCollection{ + Filters: []regolith.FilterRunner{}, + }, + ExportTarget: regolith.ExportTarget{ + Target: "local", + }, + } + data, err := json.Marshal(profile) + if err != nil { + t.Fatal(err) + } + var obj map[string]any + if err := json.Unmarshal(data, &obj); err != nil { + t.Fatal(err) + } + exportObj, ok := obj["export"].(map[string]any) + if !ok { + t.Fatalf("Expected export to marshal as an object, got: %s", data) + } + if exportObj["target"] != "local" { + t.Fatalf("Expected target local, got %v", exportObj["target"]) + } +} + +func TestRunWithMultipleExactExportTargets(t *testing.T) { + defer os.Chdir(getWdOrFatal(t)) + + tmpDir := prepareTestDirectory( + fmt.Sprintf("%s-%d", t.Name(), time.Now().UnixNano()), t) + workingDir := filepath.Join(tmpDir, "working-dir") + copyFilesOrFatal(minimalProjectPath, workingDir, t) + + config := []byte(`{ + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.2.json", + "name": "regolith_test_project", + "author": "Bedrock-OSS", + "packs": { + "behaviorPack": "./packs/BP", + "resourcePack": "./packs/RP" + }, + "regolith": { + "profiles": { + "multi": { + "filters": [], + "export": [ + { + "target": "exact", + "rpPath": "../target-a/RP", + "bpPath": "../target-a/BP" + }, + { + "target": "exact", + "rpPath": "../target-b/RP", + "bpPath": "../target-b/BP" + } + ] + } + }, + "dataPath": "./packs/data" + } + }`) + if err := os.WriteFile(filepath.Join(workingDir, "config.json"), config, 0644); err != nil { + t.Fatal("Unable to write multi-target config:", err) + } + + os.Chdir(workingDir) + if err := regolith.Run("multi", []string{}, true, ""); err != nil { + t.Fatal("First multi-target run failed:", err) + } + + for _, target := range []string{"target-a", "target-b"} { + comparePaths( + filepath.Join(workingDir, "packs", "BP"), + filepath.Join(tmpDir, target, "BP"), + t, + ) + comparePaths( + filepath.Join(workingDir, "packs", "RP"), + filepath.Join(tmpDir, target, "RP"), + t, + ) + } + + if err := regolith.Run("multi", []string{}, true, ""); err != nil { + t.Fatal("Second multi-target run failed safety checks:", err) + } + + unexpectedFile := filepath.Join(tmpDir, "target-a", "BP", "unexpected.txt") + if err := os.WriteFile(unexpectedFile, []byte("not created by regolith"), 0644); err != nil { + t.Fatal("Unable to create unexpected target file:", err) + } + if err := regolith.Run("multi", []string{}, true, ""); err == nil { + t.Fatal("Expected file protection to reject unexpected file in first target") + } +} + +func TestRunWithMultipleTargetsIgnoresSymlinkExport(t *testing.T) { + defer os.Chdir(getWdOrFatal(t)) + oldExperiments := regolith.EnabledExperiments + regolith.EnabledExperiments = []string{"symlink_export"} + t.Cleanup(func() { + regolith.EnabledExperiments = oldExperiments + }) + + tmpDir := prepareTestDirectory( + fmt.Sprintf("%s-%d", t.Name(), time.Now().UnixNano()), t) + workingDir := filepath.Join(tmpDir, "working-dir") + copyFilesOrFatal(minimalProjectPath, workingDir, t) + + config := []byte(`{ + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.2.json", + "name": "regolith_test_project", + "author": "Bedrock-OSS", + "packs": { + "behaviorPack": "./packs/BP", + "resourcePack": "./packs/RP" + }, + "regolith": { + "profiles": { + "multi": { + "filters": [], + "export": [ + { + "target": "exact", + "rpPath": "../target-a/RP", + "bpPath": "../target-a/BP" + }, + { + "target": "exact", + "rpPath": "../target-b/RP", + "bpPath": "../target-b/BP" + } + ] + } + }, + "dataPath": "./packs/data" + } + }`) + if err := os.WriteFile(filepath.Join(workingDir, "config.json"), config, 0644); err != nil { + t.Fatal("Unable to write multi-target config:", err) + } + + os.Chdir(workingDir) + if err := regolith.Run("multi", []string{}, true, ""); err != nil { + t.Fatal("Multi-target run with symlink_export enabled failed:", err) + } + + for _, tmpPack := range []string{"BP", "RP"} { + info, err := os.Lstat(filepath.Join(workingDir, ".regolith", "tmp", tmpPack)) + if err != nil { + t.Fatalf("Unable to stat tmp %s path: %v", tmpPack, err) + } + if info.Mode()&os.ModeSymlink != 0 { + t.Fatalf("Expected tmp %s path to be a directory, got symlink", tmpPack) + } + } + + for _, target := range []string{"target-a", "target-b"} { + comparePaths( + filepath.Join(workingDir, "packs", "BP"), + filepath.Join(tmpDir, target, "BP"), + t, + ) + comparePaths( + filepath.Join(workingDir, "packs", "RP"), + filepath.Join(tmpDir, target, "RP"), + t, + ) + } +} + +func TestRunWithLocalAndDevelopmentExportTargets(t *testing.T) { + defer os.Chdir(getWdOrFatal(t)) + + tmpDir := prepareTestDirectory( + fmt.Sprintf("%s-%d", t.Name(), time.Now().UnixNano()), t) + workingDir := filepath.Join(tmpDir, "working-dir") + copyFilesOrFatal(minimalProjectPath, workingDir, t) + + mojangDir := filepath.Join(tmpDir, "com.mojang") + if err := os.MkdirAll(mojangDir, 0755); err != nil { + t.Fatal("Unable to create fake com.mojang directory:", err) + } + t.Setenv("COM_MOJANG_PACKS", mojangDir) + + config := []byte(`{ + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.4.json", + "name": "regolith_test_project", + "author": "Bedrock-OSS", + "packs": { + "behaviorPack": "./packs/BP", + "resourcePack": "./packs/RP" + }, + "regolith": { + "formatVersion": "1.4.0", + "profiles": { + "local_and_development": { + "filters": [], + "export": [ + { + "target": "local" + }, + { + "target": "development", + "build": "standard" + } + ] + } + }, + "dataPath": "./packs/data" + } + }`) + if err := os.WriteFile(filepath.Join(workingDir, "config.json"), config, 0644); err != nil { + t.Fatal("Unable to write mixed-target config:", err) + } + + os.Chdir(workingDir) + if err := regolith.Run("local_and_development", []string{}, false, ""); err != nil { + t.Fatal("First mixed-target run failed:", err) + } + + expectedBp := filepath.Join(workingDir, "packs", "BP") + expectedRp := filepath.Join(workingDir, "packs", "RP") + bpName := "regolith_test_project_bp" + rpName := "regolith_test_project_rp" + + comparePaths(expectedBp, filepath.Join(workingDir, "build", bpName), t) + comparePaths(expectedRp, filepath.Join(workingDir, "build", rpName), t) + comparePaths( + expectedBp, + filepath.Join(mojangDir, "development_behavior_packs", bpName), + t, + ) + comparePaths( + expectedRp, + filepath.Join(mojangDir, "development_resource_packs", rpName), + t, + ) + + if err := regolith.Run("local_and_development", []string{}, false, ""); err != nil { + t.Fatal("Second mixed-target run failed safety checks:", err) + } +} From bd4aa0544e831707696c98a2448210727c5fe40d Mon Sep 17 00:00:00 2001 From: Nusiq Date: Fri, 1 May 2026 00:37:15 +0200 Subject: [PATCH 17/31] Moved the doc-comment of SyncDirectories() back to the function it belongs to. --- regolith/file_system.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/regolith/file_system.go b/regolith/file_system.go index 70254094..68af51f0 100644 --- a/regolith/file_system.go +++ b/regolith/file_system.go @@ -852,10 +852,6 @@ func MoveOrCopy( return nil } -// SyncDirectories copies the source to destination while checking size and modification time. -// If the file in the destination is different than the one in the source, it's overwritten, -// otherwise it's skipped (the destination file is not modified). - type syncMetadataCache struct { m sync.Map } @@ -926,6 +922,9 @@ func syncLink(srcPath, dstPath string) error { return nil } +// SyncDirectories copies the source to destination while checking size and modification time. +// If the file in the destination is different than the one in the source, it's overwritten, +// otherwise it's skipped (the destination file is not modified). func SyncDirectories( source string, destination string, makeReadOnly bool, ) error { From dc2119b5850f7e976d7ed2def3dd1cd351b71882 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Fri, 1 May 2026 16:16:34 +0200 Subject: [PATCH 18/31] Added some doc-comments. --- regolith/file_system.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/regolith/file_system.go b/regolith/file_system.go index 68af51f0..0f3716c9 100644 --- a/regolith/file_system.go +++ b/regolith/file_system.go @@ -852,10 +852,16 @@ func MoveOrCopy( return nil } +// syncMetadataCache is a thread-safe cache for file FileInfo used in +// SyncDirectories to reduce the number of os.Stat calls. type syncMetadataCache struct { m sync.Map } +// cachedMeta is a wrapper for os.FileInfo used in syncMetadataCache. The +// 'nil' value of the 'info' field indicates that the path was previously +// checked but os.Stat returned an error (file not found or permission denied). +// The errors aren't cached. type cachedMeta struct { info os.FileInfo } @@ -898,6 +904,10 @@ func removeJunctionSafe(path string) error { return os.Remove(path) } +// syncLink copies a symlink or junction from srcPath to make the dstPath +// point to the same target. If srcPath is not a symlink or junction, +// it's skipped without an error. If dstPath already exists, it's removed +// first. func syncLink(srcPath, dstPath string) error { linkTarget, err := os.Readlink(srcPath) if err != nil { @@ -973,6 +983,7 @@ func SyncDirectories( srcPath := filepath.Join(srcDir, entry.Name()) dstPath := filepath.Join(dstDir, entry.Name()) + // Symlink source if entry.Type()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { g.Go(func() error { if ctx.Err() != nil { @@ -983,6 +994,7 @@ func SyncDirectories( continue } + // Directory source srcMeta := cache.get(srcPath) if srcMeta != nil && srcMeta.IsDir() { dstMeta := cache.get(dstPath) @@ -1013,6 +1025,7 @@ func SyncDirectories( continue } + // File source g.Go(func() error { if ctx.Err() != nil { return ctx.Err() From 8d07d44c07c5e3e53dab0fdcfd17ea64d44ce2cf Mon Sep 17 00:00:00 2001 From: FrederoxDev Date: Fri, 1 May 2026 15:28:16 +0100 Subject: [PATCH 19/31] requested changes --- main.go | 9 ++++++--- regolith/experiments.go | 5 +---- regolith/export.go | 2 +- regolith/file_system.go | 26 +++++++++++++++++++------- regolith/filter.go | 1 + regolith/filter_profile.go | 1 + regolith/filter_remote.go | 1 + regolith/main_functions.go | 11 ++++++----- regolith/profile.go | 4 ++-- 9 files changed, 38 insertions(+), 22 deletions(-) diff --git a/main.go b/main.go index f1243a63..2adf3048 100644 --- a/main.go +++ b/main.go @@ -292,6 +292,7 @@ func main() { subcommands = append(subcommands, cmdInstallAll) // regolith run + var runUnsafe bool cmdRun := &cobra.Command{ Use: "run [profile_name]", Short: "Runs Regolith using specified profile", @@ -304,12 +305,14 @@ func main() { extraFilterArgs = args[1:] } env, _ := cmd.Flags().GetString("env") - err = regolith.Run(profile, extraFilterArgs, burrito.PrintStackTrace, env) + err = regolith.Run(profile, extraFilterArgs, burrito.PrintStackTrace, env, runUnsafe) }, } + cmdRun.Flags().BoolVar(&runUnsafe, "unsafe", false, "Disables file protection safety checks for faster exports") subcommands = append(subcommands, cmdRun) // regolith watch + var watchUnsafe bool cmdWatch := &cobra.Command{ Use: "watch [profile_name]", Short: "Watches project files and automatically runs Regolith when they change", @@ -322,9 +325,10 @@ func main() { extraFilterArgs = args[1:] } env, _ := cmd.Flags().GetString("env") - err = regolith.Watch(profile, extraFilterArgs, burrito.PrintStackTrace, env) + err = regolith.Watch(profile, extraFilterArgs, burrito.PrintStackTrace, env, watchUnsafe) }, } + cmdWatch.Flags().BoolVar(&watchUnsafe, "unsafe", false, "Disables file protection safety checks for faster exports") subcommands = append(subcommands, cmdWatch) // regolith apply-filter @@ -410,7 +414,6 @@ func main() { ®olith.EnabledExperiments, "experiments", nil, "Enables experimental features. Currently supported experiments:\n"+ strings.Join(experimentDescs, "\n")) - cmd.Flags().BoolVar(®olith.UnsafeMode, "unsafe", false, "Disables file protection safety checks for faster exports") } // Build and run CLI diff --git a/regolith/experiments.go b/regolith/experiments.go index 53d01634..48a84980 100644 --- a/regolith/experiments.go +++ b/regolith/experiments.go @@ -35,10 +35,7 @@ var AvailableExperiments = map[Experiment]ExperimentInfo{ SymlinkExport: {"symlink_export", symlinkExportDesc}, } -var ( - EnabledExperiments []string - UnsafeMode bool -) +var EnabledExperiments []string func IsExperimentEnabled(exp Experiment) bool { if EnabledExperiments == nil { diff --git a/regolith/export.go b/regolith/export.go index a5a07a6f..06184a26 100644 --- a/regolith/export.go +++ b/regolith/export.go @@ -247,7 +247,7 @@ func ExportProject(ctx RunContext) error { // Load edited files MeasureStart("Export - CheckDeletionSafety") editedFiles := LoadEditedFiles(dotRegolithPath) - if !IsExperimentEnabled(SymlinkExport) && !UnsafeMode { + if !IsExperimentEnabled(SymlinkExport) && !ctx.UnsafeMode { err = editedFiles.CheckDeletionSafety(rpPath, bpPath) if err != nil { return burrito.WrapErrorf( diff --git a/regolith/file_system.go b/regolith/file_system.go index 0f3716c9..d88436ce 100644 --- a/regolith/file_system.go +++ b/regolith/file_system.go @@ -896,12 +896,24 @@ func removeJunctionSafe(path string) error { return burrito.WrapErrorf(err, osStatErrorAny, path) } if linfo.Mode()&(fs.ModeSymlink|fs.ModeIrregular) != 0 { - return os.Remove(path) + if err := os.Remove(path); err != nil { + return burrito.WrapErrorf(err, + "Failed to remove junction/symlink.\nPath: %s", path) + } + return nil } if linfo.IsDir() { - return os.RemoveAll(path) + if err := os.RemoveAll(path); err != nil { + return burrito.WrapErrorf(err, + "Failed to remove directory.\nPath: %s", path) + } + return nil } - return os.Remove(path) + if err := os.Remove(path); err != nil { + return burrito.WrapErrorf(err, + "Failed to remove file.\nPath: %s", path) + } + return nil } // syncLink copies a symlink or junction from srcPath to make the dstPath @@ -915,7 +927,7 @@ func syncLink(srcPath, dstPath string) error { return nil } if err := removeJunctionSafe(dstPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, dstPath) + return burrito.PassError(err) } srcLinfo, lerr := os.Lstat(srcPath) isJunction := lerr == nil && srcLinfo.Mode()&fs.ModeIrregular != 0 @@ -961,7 +973,7 @@ func SyncDirectories( } if cache.get(dstDir) == nil { if err := removeJunctionSafe(dstDir); err != nil { - syncErr = burrito.WrapErrorf(err, osRemoveError, dstDir) + syncErr = burrito.PassError(err) return } if err := os.MkdirAll(dstDir, 0755); err != nil { @@ -1001,7 +1013,7 @@ func SyncDirectories( if dstMeta != nil && !dstMeta.IsDir() { // Destination is a file but source is a dir — remove it if err := removeJunctionSafe(dstPath); err != nil { - syncErr = burrito.WrapErrorf(err, osRemoveError, dstPath) + syncErr = burrito.PassError(err) return } cache.invalidate(dstPath) @@ -1033,7 +1045,7 @@ func SyncDirectories( dstMeta := cache.get(dstPath) if dstMeta != nil && dstMeta.IsDir() { if err := removeJunctionSafe(dstPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, dstPath) + return burrito.PassError(err) } dstMeta = nil } diff --git a/regolith/filter.go b/regolith/filter.go index 93bdb874..9e693fa5 100644 --- a/regolith/filter.go +++ b/regolith/filter.go @@ -30,6 +30,7 @@ type RunContext struct { DotRegolithPath string Settings map[string]any ExtraArguments []string + UnsafeMode bool // interruption is a channel used to receive notifications about changes // in the source files, in order to trigger a restart of the program in diff --git a/regolith/filter_profile.go b/regolith/filter_profile.go index 89f5167a..e36050a0 100644 --- a/regolith/filter_profile.go +++ b/regolith/filter_profile.go @@ -17,6 +17,7 @@ func (f *ProfileFilter) Run(context RunContext) (bool, error) { interruption: context.interruption, DotRegolithPath: context.DotRegolithPath, Settings: f.Settings, + UnsafeMode: context.UnsafeMode, }) } diff --git a/regolith/filter_remote.go b/regolith/filter_remote.go index 51f19f5e..2e60af18 100644 --- a/regolith/filter_remote.go +++ b/regolith/filter_remote.go @@ -98,6 +98,7 @@ func (f *RemoteFilter) run(context RunContext) (bool, error) { Parent: context.Parent, DotRegolithPath: context.DotRegolithPath, Settings: filter.GetSettings(), + UnsafeMode: context.UnsafeMode, } // Disabled filters are skipped disabled, err := filter.IsDisabled(runContext) diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 59d777f5..e2fb6a7a 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -217,7 +217,7 @@ func InstallAll(force, update, debug, refreshFilters bool, env string) error { // prepareRunContext prepares the context for the "regolith run" and // "regolith watch" commands. -func prepareRunContext(profileName string, extraFilterArgs []string, debug bool, env string) (*RunContext, error) { +func prepareRunContext(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool) (*RunContext, error) { InitLogging(debug) if err := loadEnvFileFromArg(env); err != nil { return nil, burrito.WrapErrorf(err, loadEnvFileFromArgError, env) @@ -264,14 +264,15 @@ func prepareRunContext(profileName string, extraFilterArgs []string, debug bool, DotRegolithPath: dotRegolithPath, Settings: map[string]any{}, ExtraArguments: extraFilterArgs, + UnsafeMode: unsafeMode, }, nil } // Run handles the "regolith run" command. It runs selected profile and exports // created resource pack and behavior pack to the target destination. -func Run(profileName string, extraFilterArgs []string, debug bool, env string) error { +func Run(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool) error { // Get the context - context, err := prepareRunContext(profileName, extraFilterArgs, debug, env) + context, err := prepareRunContext(profileName, extraFilterArgs, debug, env, unsafeMode) defer ShutdownLogging() if err != nil { return burrito.PassError(err) @@ -294,9 +295,9 @@ func Run(profileName string, extraFilterArgs []string, debug bool, env string) e // Watch handles the "regolith watch" command. It watches the project // directories, and it runs selected profile and exports created resource pack // and behavior pack to the target destination when the project changes. -func Watch(profileName string, extraFilterArgs []string, debug bool, env string) error { +func Watch(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool) error { // Get the context - context, err := prepareRunContext(profileName, extraFilterArgs, debug, env) + context, err := prepareRunContext(profileName, extraFilterArgs, debug, env, unsafeMode) defer ShutdownLogging() if err != nil { return burrito.PassError(err) diff --git a/regolith/profile.go b/regolith/profile.go index e33e7af8..11e441b6 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -176,7 +176,7 @@ func SetupTmpFiles(context RunContext) error { } else if shouldCreateSymlinks { for _, tmpPath := range []string{bpTmpPath, rpTmpPath} { if err := removeJunctionSafe(tmpPath); err != nil { - return burrito.WrapErrorf(err, osRemoveError, tmpPath) + return burrito.PassError(err) } } } @@ -189,7 +189,7 @@ func SetupTmpFiles(context RunContext) error { // Create symlinks if shouldCreateSymlinks { - if !UnsafeMode { + if !context.UnsafeMode { editedFiles := LoadEditedFiles(dotRegolithPath) err := editedFiles.CheckDeletionSafety(rpExportPath, bpExportPath) if err != nil { From d1b22437af015144d7709eb7c708d4feccdd6c50 Mon Sep 17 00:00:00 2001 From: FrederoxDev Date: Fri, 1 May 2026 15:32:48 +0100 Subject: [PATCH 20/31] move getting the flag --- main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 2adf3048..5a9b25d6 100644 --- a/main.go +++ b/main.go @@ -292,7 +292,6 @@ func main() { subcommands = append(subcommands, cmdInstallAll) // regolith run - var runUnsafe bool cmdRun := &cobra.Command{ Use: "run [profile_name]", Short: "Runs Regolith using specified profile", @@ -305,14 +304,14 @@ func main() { extraFilterArgs = args[1:] } env, _ := cmd.Flags().GetString("env") - err = regolith.Run(profile, extraFilterArgs, burrito.PrintStackTrace, env, runUnsafe) + unsafe, _ := cmd.Flags().GetBool("unsafe") + err = regolith.Run(profile, extraFilterArgs, burrito.PrintStackTrace, env, unsafe) }, } - cmdRun.Flags().BoolVar(&runUnsafe, "unsafe", false, "Disables file protection safety checks for faster exports") + cmdRun.Flags().Bool("unsafe", false, "Disables file protection safety checks for faster exports") subcommands = append(subcommands, cmdRun) // regolith watch - var watchUnsafe bool cmdWatch := &cobra.Command{ Use: "watch [profile_name]", Short: "Watches project files and automatically runs Regolith when they change", @@ -325,10 +324,11 @@ func main() { extraFilterArgs = args[1:] } env, _ := cmd.Flags().GetString("env") - err = regolith.Watch(profile, extraFilterArgs, burrito.PrintStackTrace, env, watchUnsafe) + unsafe, _ := cmd.Flags().GetBool("unsafe") + err = regolith.Watch(profile, extraFilterArgs, burrito.PrintStackTrace, env, unsafe) }, } - cmdWatch.Flags().BoolVar(&watchUnsafe, "unsafe", false, "Disables file protection safety checks for faster exports") + cmdWatch.Flags().Bool("unsafe", false, "Disables file protection safety checks for faster exports") subcommands = append(subcommands, cmdWatch) // regolith apply-filter From da620a54fc414bddbfb4690c497bc70d9aabbc07 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Fri, 1 May 2026 16:50:21 +0200 Subject: [PATCH 21/31] Fixed tests. --- test/conditional_filters_test.go | 2 +- test/custom_pack_name_test.go | 2 +- test/development_export_windows_test.go | 2 +- test/file_protection_test.go | 10 +++++----- test/filter_async_test.go | 2 +- test/local_filters_test.go | 10 +++++----- test/pre_post_shell_os_specific_test.go | 2 +- test/pre_post_shell_test.go | 2 +- test/remote_filters_test.go | 4 ++-- test/size_time_check_optimization_method_test.go | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test/conditional_filters_test.go b/test/conditional_filters_test.go index d5d520f7..31a3bde9 100644 --- a/test/conditional_filters_test.go +++ b/test/conditional_filters_test.go @@ -29,7 +29,7 @@ func TestConditionalFilter(t *testing.T) { // THE TEST t.Log("Running Regolith with a conditional filter...") - if err := regolith.Run("default", []string{}, true, ""); err != nil { + if err := regolith.Run("default", []string{}, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/custom_pack_name_test.go b/test/custom_pack_name_test.go index 91cdc116..ede3e805 100644 --- a/test/custom_pack_name_test.go +++ b/test/custom_pack_name_test.go @@ -30,7 +30,7 @@ func TestCustomPackName(t *testing.T) { // THE TEST t.Log("Running Regolith with a conditional filter...") - if err := regolith.Run("default", []string{}, true, ""); err != nil { + if err := regolith.Run("default", []string{}, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/development_export_windows_test.go b/test/development_export_windows_test.go index 4163f87f..66f1d703 100644 --- a/test/development_export_windows_test.go +++ b/test/development_export_windows_test.go @@ -103,7 +103,7 @@ func _testCustomDevelopmentExportLocation( // THE TEST t.Log("Testing the 'regolith run' command...") - err = regolith.Run(profileToRun, []string{}, true, "") + err = regolith.Run(profileToRun, []string{}, true, "", false) if err != nil { t.Fatal("'regolith run' failed:", err) } diff --git a/test/file_protection_test.go b/test/file_protection_test.go index 59fbeab2..1a281746 100644 --- a/test/file_protection_test.go +++ b/test/file_protection_test.go @@ -34,18 +34,18 @@ func TestSwitchingExportTargets(t *testing.T) { // Run Regolith with targets: A, B, A t.Log("Testing the 'regolith run' with changing export targets...") t.Log("Running Regolith with target A...") - err := regolith.Run("exact_export_A", []string{}, true, "") + err := regolith.Run("exact_export_A", []string{}, true, "", false) if err != nil { t.Fatal( "Unable RunProfile failed on first attempt to export to A:", err) } t.Log("Running Regolith with target B...") - err = regolith.Run("exact_export_B", []string{}, true, "") + err = regolith.Run("exact_export_B", []string{}, true, "", false) if err != nil { t.Fatal("Unable RunProfile failed on attempt to export to B:", err) } t.Log("Running Regolith with target A (2nd time)...") - err = regolith.Run("exact_export_A", []string{}, true, "") + err = regolith.Run("exact_export_A", []string{}, true, "", false) if err != nil { t.Fatal( "Unable RunProfile failed on second attempt to export to A:", err) @@ -78,7 +78,7 @@ func TestTriggerFileProtection(t *testing.T) { // 1. Run Regolith (export to A) t.Log("Testing the 'regolith run' with file protection...") t.Log("Running Regolith...") - err := regolith.Run("exact_export_A", []string{}, true, "") + err := regolith.Run("exact_export_A", []string{}, true, "", false) if err != nil { t.Fatal( "Unable RunProfile failed on first attempt to export to A:", err) @@ -94,7 +94,7 @@ func TestTriggerFileProtection(t *testing.T) { // 3. Run Regolith (export to A), expect failure. t.Log("Running Regolith (this should be stopped by file protection system)...") - err = regolith.Run("exact_export_A", []string{}, true, "") + err = regolith.Run("exact_export_A", []string{}, true, "", false) if err == nil { t.Fatal("Expected RunProfile to fail on second attempt to export to A") } diff --git a/test/filter_async_test.go b/test/filter_async_test.go index c6e6736b..6ace4dbe 100644 --- a/test/filter_async_test.go +++ b/test/filter_async_test.go @@ -30,7 +30,7 @@ func TestAsyncFilter(t *testing.T) { t.Log("Running Regolith with a conditional filter...") start := time.Now() - if err := regolith.Run("default", []string{}, true, ""); err != nil { + if err := regolith.Run("default", []string{}, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } duration := time.Since(start) diff --git a/test/local_filters_test.go b/test/local_filters_test.go index 477fc93c..6f287e87 100644 --- a/test/local_filters_test.go +++ b/test/local_filters_test.go @@ -44,7 +44,7 @@ func TestRegolithRunMissingRp(t *testing.T) { os.Chdir(tmpDir) // THE TEST - err := regolith.Run("dev", []string{}, true, "") + err := regolith.Run("dev", []string{}, true, "", false) if err != nil { t.Fatal("'regolith run' failed:", err) } @@ -71,7 +71,7 @@ func TestLocalRequirementsInstallAndRun(t *testing.T) { t.Fatal("'regolith install-all' failed", err.Error()) } t.Log("Testing the 'regolith run' command...") - if err := regolith.Run("dev", []string{}, true, ""); err != nil { + if err := regolith.Run("dev", []string{}, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } } @@ -95,7 +95,7 @@ func TestExeFilterRun(t *testing.T) { // THE TEST t.Log("Testing the 'regolith run' command...") - if err := regolith.Run("dev", []string{}, true, ""); err != nil { + if err := regolith.Run("dev", []string{}, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } // TEST EVALUATION @@ -125,7 +125,7 @@ func TestProfileFilterRun(t *testing.T) { // THE TEST // Invalid profile (shoud fail) t.Log("Running invalid profile filter with circular dependencies (this should fail).") - err := regolith.Run("invalid_circular_profile_1", []string{}, true, "") + err := regolith.Run("invalid_circular_profile_1", []string{}, true, "", false) if err == nil { t.Fatal("'regolith run' didn't return an error after running" + " a circular profile filter.") @@ -134,7 +134,7 @@ func TestProfileFilterRun(t *testing.T) { } // Valid profile (should succeed) t.Log("Running valid profile filter.") - err = regolith.Run("correct_nested_profile", []string{}, true, "") + err = regolith.Run("correct_nested_profile", []string{}, true, "", false) if err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/pre_post_shell_os_specific_test.go b/test/pre_post_shell_os_specific_test.go index 2381fbf6..1ea5a4d7 100644 --- a/test/pre_post_shell_os_specific_test.go +++ b/test/pre_post_shell_os_specific_test.go @@ -27,7 +27,7 @@ func TestPrePostShellCommandsOSSpecific(t *testing.T) { // THE TEST t.Log("Testing OS-specific preShell and postShell commands...") - if err := regolith.Run("default", nil, true, ""); err != nil { + if err := regolith.Run("default", nil, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/pre_post_shell_test.go b/test/pre_post_shell_test.go index 50be6cc8..2d619da4 100644 --- a/test/pre_post_shell_test.go +++ b/test/pre_post_shell_test.go @@ -27,7 +27,7 @@ func TestPrePostShellCommands(t *testing.T) { // THE TEST t.Log("Testing the 'regolith run' command with preShell and postShell...") - if err := regolith.Run("default", nil, true, ""); err != nil { + if err := regolith.Run("default", nil, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/remote_filters_test.go b/test/remote_filters_test.go index 4b26ee01..7699773d 100644 --- a/test/remote_filters_test.go +++ b/test/remote_filters_test.go @@ -38,7 +38,7 @@ func TestInstallAllAndRun(t *testing.T) { } t.Log("Testing the 'regolith run' command...") - err = regolith.Run("dev", []string{}, true, "") + err = regolith.Run("dev", []string{}, true, "", false) if err != nil { t.Fatal("'regolith run' failed:", err) } @@ -78,7 +78,7 @@ func TestDataModifyRemoteFilter(t *testing.T) { } t.Log("Testing the 'regolith run' command...") - err = regolith.Run("default", []string{}, true, "") + err = regolith.Run("default", []string{}, true, "", false) if err != nil { t.Fatal("'regolith run' failed:", err) } diff --git a/test/size_time_check_optimization_method_test.go b/test/size_time_check_optimization_method_test.go index 16c8c51b..059794e0 100644 --- a/test/size_time_check_optimization_method_test.go +++ b/test/size_time_check_optimization_method_test.go @@ -51,7 +51,7 @@ func TestSizeTimeCheckOptimizationCorectness(t *testing.T) { // Run the project t.Log("Running Regolith...") - if err := regolith.Run("default", []string{}, true, ""); err != nil { + if err := regolith.Run("default", []string{}, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } // TEST EVALUATION @@ -94,7 +94,7 @@ func TestSizeTimeCheckOptimizationSpeed(t *testing.T) { // Start the timer start := time.Now() - if err := regolith.Run("default", []string{}, true, ""); err != nil { + if err := regolith.Run("default", []string{}, true, "", false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } // Stop the timer From cff13a7ceae4fe214f1b0ed01ba554db93912fb7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 1 May 2026 15:00:31 +0000 Subject: [PATCH 22/31] Generated CREDITS.csv --- CREDITS.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CREDITS.csv b/CREDITS.csv index 77d22b78..04d5ee51 100644 --- a/CREDITS.csv +++ b/CREDITS.csv @@ -22,6 +22,6 @@ go.uber.org/zap,https://github.com/uber-go/zap/blob/v1.23.0/LICENSE.txt,MIT golang.org/x/crypto,https://cs.opensource.google/go/x/crypto/+/v0.1.0:LICENSE,BSD-3-Clause golang.org/x/exp,https://cs.opensource.google/go/x/exp/+/aae9b4e6:LICENSE,BSD-3-Clause golang.org/x/mod/semver,https://cs.opensource.google/go/x/mod/+/v0.6.0:LICENSE,BSD-3-Clause -golang.org/x/sync,https://cs.opensource.google/go/x/sync/+/v0.8.0:LICENSE,BSD-3-Clause +golang.org/x/sync,https://cs.opensource.google/go/x/sync/+/v0.20.0:LICENSE,BSD-3-Clause golang.org/x/sys/unix,https://cs.opensource.google/go/x/sys/+/v0.24.0:LICENSE,BSD-3-Clause golang.org/x/text,https://cs.opensource.google/go/x/text/+/v0.6.0:LICENSE,BSD-3-Clause From 2ad27416f875769ae535e8fd7626e8318cde0d0a Mon Sep 17 00:00:00 2001 From: Hydrogen <96733109+dev-hydrogen@users.noreply.github.com> Date: Mon, 11 May 2026 17:18:53 +0300 Subject: [PATCH 23/31] export path collision check, use ExportTarget instead of ExportTargets, add comment to removeAllForTest and add collision check test --- regolith/export.go | 56 +++++++++++++++++++++++++- regolith/main_functions.go | 2 +- regolith/profile.go | 36 ++--------------- test/common.go | 3 ++ test/export_targets_test.go | 78 +++++++++++++++++++++++++++++++------ 5 files changed, 130 insertions(+), 45 deletions(-) diff --git a/regolith/export.go b/regolith/export.go index c2fe6f93..23a687a9 100644 --- a/regolith/export.go +++ b/regolith/export.go @@ -1,6 +1,7 @@ package regolith import ( + "fmt" "os" "path/filepath" "runtime" @@ -232,6 +233,51 @@ type resolvedExportTarget struct { rpPath string } +func normalizeExportPathForCollision(path string) (string, error) { + absPath, err := filepath.Abs(filepath.Clean(path)) + if err != nil { + return "", burrito.WrapErrorf(err, filepathAbsError, path) + } + if resolvedPath, err := filepath.EvalSymlinks(absPath); err == nil { + absPath = resolvedPath + } + if runtime.GOOS == "windows" { + absPath = strings.ToLower(absPath) + } + return absPath, nil +} + +func pathContains(parent, child string) bool { + rel, err := filepath.Rel(parent, child) + if err != nil { + return false + } + return rel != "." && rel != ".." && + !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && + !filepath.IsAbs(rel) +} + +func checkExportPathCollision(seen map[string]string, path, label string) error { + normalizedPath, err := normalizeExportPathForCollision(path) + if err != nil { + return burrito.PassError(err) + } + for seenPath, seenLabel := range seen { + if normalizedPath == seenPath || + pathContains(normalizedPath, seenPath) || + pathContains(seenPath, normalizedPath) { + return burrito.WrappedErrorf( + "Export path collision detected.\n"+ + "First path: %s\n"+ + "Second path: %s\n"+ + "Overlapping path: %s", + seenLabel, label, normalizedPath) + } + } + seen[normalizedPath] = label + return nil +} + // ExportProject copies files from the tmp paths (tmp/BP and tmp/RP) into // the project's export targets. The paths are generated with GetExportPaths. func ExportProject(ctx RunContext) error { @@ -244,11 +290,19 @@ func ExportProject(ctx RunContext) error { // keeps failure atomic when a later target has an invalid path or unsafe // existing files. var activeTargets []resolvedExportTarget - for _, exportTarget := range profile.activeExportTargets() { + seenExportPaths := make(map[string]string) + for i, exportTarget := range profile.activeExportTargets() { bpPath, rpPath, err := GetExportPaths(exportTarget, ctx) if err != nil { return burrito.WrapError(err, getExportPathsError) } + targetLabel := fmt.Sprintf("export target %d (%s)", i+1, exportTarget.Target) + if err := checkExportPathCollision(seenExportPaths, bpPath, targetLabel+" behavior pack: "+bpPath); err != nil { + return burrito.PassError(err) + } + if err := checkExportPathCollision(seenExportPaths, rpPath, targetLabel+" resource pack: "+rpPath); err != nil { + return burrito.PassError(err) + } activeTargets = append(activeTargets, resolvedExportTarget{ target: exportTarget, bpPath: bpPath, diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 8d96c57b..3c162b43 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -488,7 +488,7 @@ func Init(debug, force bool, env string) error { FilterCollection: FilterCollection{ Filters: []FilterRunner{}, }, - ExportTargets: ExportTargets{ + ExportTarget: ExportTargets{ { Target: "development", Build: "standard", diff --git a/regolith/profile.go b/regolith/profile.go index 628b5614..ca26c516 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -562,23 +562,13 @@ func (sc *ShellCommands) GetCommandsForCurrentOS() []string { // When editing, adjust ProfileFromObject function as well. type Profile struct { FilterCollection - ExportTargets ExportTargets `json:"export,omitzero"` - // Deprecated: use ExportTargets. This field is kept as a compatibility - // fallback for Go callers that still build Profile values with one export - // target. - ExportTarget ExportTarget `json:"-"` + ExportTarget ExportTargets `json:"export,omitzero"` PreShell ShellCommands `json:"preShell,omitzero"` PostShell ShellCommands `json:"postShell,omitzero"` } func (p Profile) exportTargets() ExportTargets { - if len(p.ExportTargets) > 0 { - return p.ExportTargets - } - if p.ExportTarget.Target != "" { - return ExportTargets{p.ExportTarget} - } - return nil + return p.ExportTarget } func (p Profile) activeExportTargets() ExportTargets { @@ -592,21 +582,6 @@ func (p Profile) activeExportTargets() ExportTargets { return activeTargets } -func (p Profile) MarshalJSON() ([]byte, error) { - type profileJSON struct { - Filters []FilterRunner `json:"filters"` - ExportTargets ExportTargets `json:"export,omitzero"` - PreShell ShellCommands `json:"preShell,omitzero"` - PostShell ShellCommands `json:"postShell,omitzero"` - } - return json.Marshal(profileJSON{ - Filters: p.Filters, - ExportTargets: p.exportTargets(), - PreShell: p.PreShell, - PostShell: p.PostShell, - }) -} - func shellCommandsFromObject(obj map[string]any, key string) (ShellCommands, error) { var result ShellCommands if shellObj, ok := obj[key]; ok { @@ -701,7 +676,7 @@ func ProfileFromObject( } result.Filters = append(result.Filters, filterRunner) } - // ExportTargets + // ExportTarget exportValue, ok := obj["export"] if !ok { return result, burrito.WrappedErrorf(jsonPathMissingError, "export") @@ -710,10 +685,7 @@ func ProfileFromObject( if err != nil { return result, burrito.PassError(err) } - result.ExportTargets = exportTargets - if len(exportTargets) > 0 { - result.ExportTarget = exportTargets[0] - } + result.ExportTarget = exportTargets // PreShell and PostShell preShell, err := shellCommandsFromObject(obj, "preShell") if err != nil { diff --git a/test/common.go b/test/common.go index 2fe5ff12..af570b03 100644 --- a/test/common.go +++ b/test/common.go @@ -249,6 +249,9 @@ func prepareTestDirectory(path string, t *testing.T) string { } func removeAllForTest(path string) error { + // Some tests export packs with readOnly enabled. On Windows, rerunning + // those tests can leave read-only files in test_results, so reset + // permissions before deleting the old test directory. var err error for range 3 { _ = filepath.WalkDir(path, func(currPath string, entry fs.DirEntry, walkErr error) error { diff --git a/test/export_targets_test.go b/test/export_targets_test.go index f401ff32..41b06af2 100644 --- a/test/export_targets_test.go +++ b/test/export_targets_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "time" @@ -173,7 +174,7 @@ func TestExportTargets_RoundTrip(t *testing.T) { } } -func TestProfileFromObject_SingleExportSetsCompatibilityField(t *testing.T) { +func TestProfileFromObject_SingleExportSetsExportTarget(t *testing.T) { profile, err := regolith.ProfileFromObject( map[string]any{ "filters": []any{}, @@ -186,24 +187,21 @@ func TestProfileFromObject_SingleExportSetsCompatibilityField(t *testing.T) { if err != nil { t.Fatal(err) } - if len(profile.ExportTargets) != 1 { - t.Fatalf("Expected 1 target, got %d", len(profile.ExportTargets)) + if len(profile.ExportTarget) != 1 { + t.Fatalf("Expected 1 target, got %d", len(profile.ExportTarget)) } - if profile.ExportTargets[0].Target != "local" { - t.Fatalf("Expected ExportTargets[0] to be local, got %q", profile.ExportTargets[0].Target) - } - if profile.ExportTarget.Target != "local" { - t.Fatalf("Expected deprecated ExportTarget fallback to be local, got %q", profile.ExportTarget.Target) + if profile.ExportTarget[0].Target != "local" { + t.Fatalf("Expected ExportTarget[0] to be local, got %q", profile.ExportTarget[0].Target) } } -func TestProfileMarshalJSON_DeprecatedExportTargetFallback(t *testing.T) { +func TestProfileMarshalJSON_SingleExportTarget(t *testing.T) { profile := regolith.Profile{ FilterCollection: regolith.FilterCollection{ Filters: []regolith.FilterRunner{}, }, - ExportTarget: regolith.ExportTarget{ - Target: "local", + ExportTarget: regolith.ExportTargets{ + {Target: "local"}, }, } data, err := json.Marshal(profile) @@ -295,6 +293,64 @@ func TestRunWithMultipleExactExportTargets(t *testing.T) { } } +func TestRunWithOverlappingExportTargetsFails(t *testing.T) { + defer os.Chdir(getWdOrFatal(t)) + + tmpDir := prepareTestDirectory( + fmt.Sprintf("%s-%d", t.Name(), time.Now().UnixNano()), t) + workingDir := filepath.Join(tmpDir, "working-dir") + copyFilesOrFatal(minimalProjectPath, workingDir, t) + + config := []byte(`{ + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.2.json", + "name": "regolith_test_project", + "author": "Bedrock-OSS", + "packs": { + "behaviorPack": "./packs/BP", + "resourcePack": "./packs/RP" + }, + "regolith": { + "profiles": { + "multi": { + "filters": [], + "export": [ + { + "target": "exact", + "rpPath": "../target-a/RP", + "bpPath": "../target-a/BP" + }, + { + "target": "exact", + "rpPath": "../target-b/RP", + "bpPath": "../target-a/BP/nested" + } + ] + } + }, + "dataPath": "./packs/data" + } + }`) + if err := os.WriteFile(filepath.Join(workingDir, "config.json"), config, 0644); err != nil { + t.Fatal("Unable to write overlapping-target config:", err) + } + + os.Chdir(workingDir) + err := regolith.Run("multi", []string{}, true, "", false) + if err == nil { + t.Fatal("Expected overlapping export paths to fail") + } + t.Logf("Got expected export path collision error:\n%v", err) + if !strings.Contains(err.Error(), "Export path collision detected") { + t.Fatalf("Expected export path collision error, got: %v", err) + } + if _, err := os.Stat(filepath.Join(tmpDir, "target-a")); !os.IsNotExist(err) { + t.Fatal("Expected export to fail before creating target-a") + } + if _, err := os.Stat(filepath.Join(tmpDir, "target-b")); !os.IsNotExist(err) { + t.Fatal("Expected export to fail before creating target-b") + } +} + func TestRunWithMultipleTargetsIgnoresSymlinkExport(t *testing.T) { defer os.Chdir(getWdOrFatal(t)) oldExperiments := regolith.EnabledExperiments From 00f42c59424e97f3e777ef7ec51cac612bb4130c Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 14 May 2026 00:04:25 +0200 Subject: [PATCH 24/31] AI commit: Move experimental features out of experiments, make size_time_check a default, fix bug that caused using size_time_check in some places where it shouldn't be used. --- main.go | 25 +++++++--------- regolith/experiments.go | 26 +--------------- regolith/export.go | 8 ++--- regolith/filter.go | 20 +++++++------ regolith/main_functions.go | 30 ++++++++++--------- regolith/profile.go | 16 +++++++--- test/conditional_filters_test.go | 2 +- test/custom_pack_name_test.go | 2 +- test/development_export_windows_test.go | 2 +- test/export_targets_test.go | 21 +++++-------- test/file_protection_test.go | 10 +++---- test/filter_async_test.go | 2 +- test/local_filters_test.go | 10 +++---- test/pre_post_shell_os_specific_test.go | 2 +- test/pre_post_shell_test.go | 2 +- test/remote_filters_test.go | 4 +-- ...ize_time_check_optimization_method_test.go | 10 +++---- 17 files changed, 85 insertions(+), 107 deletions(-) diff --git a/main.go b/main.go index 5a9b25d6..a5d4c0a1 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "strings" "github.com/Bedrock-OSS/go-burrito/burrito" "github.com/stirante/go-simple-eval/eval" @@ -292,6 +291,7 @@ func main() { subcommands = append(subcommands, cmdInstallAll) // regolith run + var symlinkExport, disableSizeTimeCheck bool cmdRun := &cobra.Command{ Use: "run [profile_name]", Short: "Runs Regolith using specified profile", @@ -305,10 +305,14 @@ func main() { } env, _ := cmd.Flags().GetString("env") unsafe, _ := cmd.Flags().GetBool("unsafe") - err = regolith.Run(profile, extraFilterArgs, burrito.PrintStackTrace, env, unsafe) + symlink, _ := cmd.Flags().GetBool("symlink-export") + disableStc, _ := cmd.Flags().GetBool("disable-size-time-check") + err = regolith.Run(profile, extraFilterArgs, burrito.PrintStackTrace, env, unsafe, symlink, disableStc) }, } cmdRun.Flags().Bool("unsafe", false, "Disables file protection safety checks for faster exports") + cmdRun.Flags().BoolVar(&symlinkExport, "symlink-export", false, "Creates links from the tmp directory to the export target so that files written to tmp are immediately reflected in the export location.") + cmdRun.Flags().BoolVar(&disableSizeTimeCheck, "disable-size-time-check", false, "Disables the size and modification time check optimization for file exporting.") subcommands = append(subcommands, cmdRun) // regolith watch @@ -325,10 +329,14 @@ func main() { } env, _ := cmd.Flags().GetString("env") unsafe, _ := cmd.Flags().GetBool("unsafe") - err = regolith.Watch(profile, extraFilterArgs, burrito.PrintStackTrace, env, unsafe) + symlink, _ := cmd.Flags().GetBool("symlink-export") + disableStc, _ := cmd.Flags().GetBool("disable-size-time-check") + err = regolith.Watch(profile, extraFilterArgs, burrito.PrintStackTrace, env, unsafe, symlink, disableStc) }, } cmdWatch.Flags().Bool("unsafe", false, "Disables file protection safety checks for faster exports") + cmdWatch.Flags().BoolVar(&symlinkExport, "symlink-export", false, "Creates links from the tmp directory to the export target so that files written to tmp are immediately reflected in the export location.") + cmdWatch.Flags().BoolVar(&disableSizeTimeCheck, "disable-size-time-check", false, "Disables the size and modification time check optimization for file exporting.") subcommands = append(subcommands, cmdWatch) // regolith apply-filter @@ -400,20 +408,9 @@ func main() { } subcommands = append(subcommands, cmdUpdateResolvers) - // Generate the description for the experiments - experimentDescs := make([]string, len(regolith.AvailableExperiments)) - for i, experiment := range regolith.AvailableExperiments { - experimentDescs[i] = "- " + experiment.Name + " - " + strings.Trim(experiment.Description, "\n") - } - - // add --debug, --timings and --experiment flag to every command for _, cmd := range subcommands { cmd.Flags().BoolVarP(&burrito.PrintStackTrace, "debug", "", false, "Enables debugging") cmd.Flags().BoolVarP(®olith.EnableTimings, "timings", "", false, "Enables timing information") - cmd.Flags().StringSliceVar( - ®olith.EnabledExperiments, "experiments", nil, - "Enables experimental features. Currently supported experiments:\n"+ - strings.Join(experimentDescs, "\n")) } // Build and run CLI diff --git a/regolith/experiments.go b/regolith/experiments.go index 48a84980..5c7da202 100644 --- a/regolith/experiments.go +++ b/regolith/experiments.go @@ -4,36 +4,12 @@ import "slices" type Experiment int -const ( - // SizeTimeCheck is an experiment that checks the size and modification time when exporting - SizeTimeCheck Experiment = iota - // SymlinkExport links the temporary build directory with the export - // target using hard links when possible. - SymlinkExport -) - -// The descriptions shouldn't be too wide, the text with their description is -// indented a lot. -const sizeTimeCheckDesc = ` -Activates optimization for file exporting by checking the size and -modification time of files before exporting, and only exporting if -the file has changed. This experiment applies to 'run' and 'watch' -commands. -` - -const symlinkExportDesc = ` -Creates links from the tmp directory to the export target so that files -written to tmp are immediately reflected in the export location.` - type ExperimentInfo struct { Name string Description string } -var AvailableExperiments = map[Experiment]ExperimentInfo{ - SizeTimeCheck: {"size_time_check", sizeTimeCheckDesc}, - SymlinkExport: {"symlink_export", symlinkExportDesc}, -} +var AvailableExperiments = map[Experiment]ExperimentInfo{} var EnabledExperiments []string diff --git a/regolith/export.go b/regolith/export.go index 23a687a9..0dfce37b 100644 --- a/regolith/export.go +++ b/regolith/export.go @@ -314,7 +314,7 @@ func ExportProject(ctx RunContext) error { return nil } dotRegolithPath := ctx.DotRegolithPath - useSymlink := IsExperimentEnabled(SymlinkExport) && len(activeTargets) == 1 + useSymlink := ctx.SymlinkExport && len(activeTargets) == 1 editedFiles := LoadEditedFiles(dotRegolithPath) if !useSymlink && !ctx.UnsafeMode { MeasureStart("Export - CheckDeletionSafety") @@ -330,7 +330,7 @@ func ExportProject(ctx RunContext) error { for i, exportTarget := range activeTargets { // Symlink export already placed files for the only active target. if useSymlink && i == 0 { - Logger.Debugf("SymlinkExport experiment is enabled. Skipping RP and BP export.") + Logger.Debugf("Symlink export is enabled. Skipping RP and BP export.") } else { // Move is only safe when there is exactly one active target // and symlink export is off, since tmp/ is the sole source and @@ -392,7 +392,7 @@ func exportProjectRpAndBp(exportTarget ExportTarget, rpPath, bpPath string, ctx dotRegolithPath := ctx.DotRegolithPath var err error - if !IsExperimentEnabled(SizeTimeCheck) { + if ctx.DisableSizeTimeCheck { MeasureStart("Export - Clean") if err := removeJunctionSafe(bpPath); err != nil { return burrito.WrapErrorf( @@ -425,7 +425,7 @@ func exportProjectRpAndBp(exportTarget ExportTarget, rpPath, bpPath string, ctx wg.Go(func() { Logger.Infof("Exporting %s pack to \"%s\".", packType, packPath) var e error - if IsExperimentEnabled(SizeTimeCheck) { + if !ctx.DisableSizeTimeCheck { e = SyncDirectories(filepath.Join(absWorkingDir, subpathInTmp), packPath, exportTarget.ReadOnly) } else if allowMove { e = MoveOrCopy(filepath.Join(absWorkingDir, subpathInTmp), packPath, exportTarget.ReadOnly, true) diff --git a/regolith/filter.go b/regolith/filter.go index 9e693fa5..41588686 100644 --- a/regolith/filter.go +++ b/regolith/filter.go @@ -22,15 +22,17 @@ type Filter struct { } type RunContext struct { - Initial bool - AbsoluteLocation string - Config *Config - Profile string - Parent *RunContext - DotRegolithPath string - Settings map[string]any - ExtraArguments []string - UnsafeMode bool + Initial bool + AbsoluteLocation string + Config *Config + Profile string + Parent *RunContext + DotRegolithPath string + Settings map[string]any + ExtraArguments []string + UnsafeMode bool + SymlinkExport bool + DisableSizeTimeCheck bool // interruption is a channel used to receive notifications about changes // in the source files, in order to trigger a restart of the program in diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 3c162b43..63ececc3 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -217,7 +217,7 @@ func InstallAll(force, update, debug, refreshFilters bool, env string) error { // prepareRunContext prepares the context for the "regolith run" and // "regolith watch" commands. -func prepareRunContext(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool) (*RunContext, error) { +func prepareRunContext(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool, symlinkExport bool, disableSizeTimeCheck bool) (*RunContext, error) { InitLogging(debug) if err := loadEnvFileFromArg(env); err != nil { return nil, burrito.WrapErrorf(err, loadEnvFileFromArgError, env) @@ -256,23 +256,25 @@ func prepareRunContext(profileName string, extraFilterArgs []string, debug bool, } path, _ := filepath.Abs(".") return &RunContext{ - Initial: true, - AbsoluteLocation: path, - Config: config, - Parent: nil, - Profile: profileName, - DotRegolithPath: dotRegolithPath, - Settings: map[string]any{}, - ExtraArguments: extraFilterArgs, - UnsafeMode: unsafeMode, + Initial: true, + AbsoluteLocation: path, + Config: config, + Parent: nil, + Profile: profileName, + DotRegolithPath: dotRegolithPath, + Settings: map[string]any{}, + ExtraArguments: extraFilterArgs, + UnsafeMode: unsafeMode, + SymlinkExport: symlinkExport, + DisableSizeTimeCheck: disableSizeTimeCheck, }, nil } // Run handles the "regolith run" command. It runs selected profile and exports // created resource pack and behavior pack to the target destination. -func Run(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool) error { +func Run(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool, symlinkExport bool, disableSizeTimeCheck bool) error { // Get the context - context, err := prepareRunContext(profileName, extraFilterArgs, debug, env, unsafeMode) + context, err := prepareRunContext(profileName, extraFilterArgs, debug, env, unsafeMode, symlinkExport, disableSizeTimeCheck) defer ShutdownLogging() if err != nil { return burrito.PassError(err) @@ -295,9 +297,9 @@ func Run(profileName string, extraFilterArgs []string, debug bool, env string, u // Watch handles the "regolith watch" command. It watches the project // directories, and it runs selected profile and exports created resource pack // and behavior pack to the target destination when the project changes. -func Watch(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool) error { +func Watch(profileName string, extraFilterArgs []string, debug bool, env string, unsafeMode bool, symlinkExport bool, disableSizeTimeCheck bool) error { // Get the context - context, err := prepareRunContext(profileName, extraFilterArgs, debug, env, unsafeMode) + context, err := prepareRunContext(profileName, extraFilterArgs, debug, env, unsafeMode, symlinkExport, disableSizeTimeCheck) defer ShutdownLogging() if err != nil { return burrito.PassError(err) diff --git a/regolith/profile.go b/regolith/profile.go index ca26c516..3ee89241 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -119,8 +119,8 @@ func SetupTmpFiles(context RunContext) error { config := *context.Config dotRegolithPath := context.DotRegolithPath start := time.Now() - useSizeTimeCheck := IsExperimentEnabled(SizeTimeCheck) - useSymlinkExport := IsExperimentEnabled(SymlinkExport) + useSizeTimeCheck := !context.DisableSizeTimeCheck + useSymlinkExport := context.SymlinkExport absTmpPath, err := GetAbsoluteWorkingDirectory(dotRegolithPath) if err != nil { return burrito.WrapError(err, getAbsoluteWorkingDirectoryError) @@ -139,7 +139,7 @@ func SetupTmpFiles(context RunContext) error { activeTargets := profile.activeExportTargets() if len(activeTargets) != 1 { if len(activeTargets) > 1 { - Logger.Debugf("SymlinkExport experiment is enabled but the profile has multiple active export targets. Using regular export.") + Logger.Debugf("Symlink export is enabled but the profile has multiple active export targets. Using regular export.") } useSymlinkExport = false } else { @@ -238,11 +238,19 @@ func SetupTmpFiles(context RunContext) error { } } } else if stats.IsDir() { - if useSizeTimeCheck || useSymlinkExport { + if useSizeTimeCheck { err = SyncDirectories(path, p, false) if err != nil { return burrito.WrapError(err, "Failed to export behavior pack.") } + } else if useSymlinkExport { + err = copy.Copy( + path, + p, + copy.Options{PreserveTimes: false, Sync: true}) + if err != nil { + return burrito.WrapErrorf(err, osCopyError, path, p) + } } else { err = copy.Copy( path, diff --git a/test/conditional_filters_test.go b/test/conditional_filters_test.go index 31a3bde9..0f575edc 100644 --- a/test/conditional_filters_test.go +++ b/test/conditional_filters_test.go @@ -29,7 +29,7 @@ func TestConditionalFilter(t *testing.T) { // THE TEST t.Log("Running Regolith with a conditional filter...") - if err := regolith.Run("default", []string{}, true, "", false); err != nil { + if err := regolith.Run("default", []string{}, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/custom_pack_name_test.go b/test/custom_pack_name_test.go index ede3e805..02b9a314 100644 --- a/test/custom_pack_name_test.go +++ b/test/custom_pack_name_test.go @@ -30,7 +30,7 @@ func TestCustomPackName(t *testing.T) { // THE TEST t.Log("Running Regolith with a conditional filter...") - if err := regolith.Run("default", []string{}, true, "", false); err != nil { + if err := regolith.Run("default", []string{}, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/development_export_windows_test.go b/test/development_export_windows_test.go index 66f1d703..61b7dac9 100644 --- a/test/development_export_windows_test.go +++ b/test/development_export_windows_test.go @@ -103,7 +103,7 @@ func _testCustomDevelopmentExportLocation( // THE TEST t.Log("Testing the 'regolith run' command...") - err = regolith.Run(profileToRun, []string{}, true, "", false) + err = regolith.Run(profileToRun, []string{}, true, "", false, false, false) if err != nil { t.Fatal("'regolith run' failed:", err) } diff --git a/test/export_targets_test.go b/test/export_targets_test.go index 41b06af2..1ee83750 100644 --- a/test/export_targets_test.go +++ b/test/export_targets_test.go @@ -263,7 +263,7 @@ func TestRunWithMultipleExactExportTargets(t *testing.T) { } os.Chdir(workingDir) - if err := regolith.Run("multi", []string{}, true, "", false); err != nil { + if err := regolith.Run("multi", []string{}, true, "", false, false, false); err != nil { t.Fatal("First multi-target run failed:", err) } @@ -280,7 +280,7 @@ func TestRunWithMultipleExactExportTargets(t *testing.T) { ) } - if err := regolith.Run("multi", []string{}, true, "", false); err != nil { + if err := regolith.Run("multi", []string{}, true, "", false, false, false); err != nil { t.Fatal("Second multi-target run failed safety checks:", err) } @@ -288,7 +288,7 @@ func TestRunWithMultipleExactExportTargets(t *testing.T) { if err := os.WriteFile(unexpectedFile, []byte("not created by regolith"), 0644); err != nil { t.Fatal("Unable to create unexpected target file:", err) } - if err := regolith.Run("multi", []string{}, true, "", false); err == nil { + if err := regolith.Run("multi", []string{}, true, "", false, false, false); err == nil { t.Fatal("Expected file protection to reject unexpected file in first target") } } @@ -335,7 +335,7 @@ func TestRunWithOverlappingExportTargetsFails(t *testing.T) { } os.Chdir(workingDir) - err := regolith.Run("multi", []string{}, true, "", false) + err := regolith.Run("multi", []string{}, true, "", false, false, false) if err == nil { t.Fatal("Expected overlapping export paths to fail") } @@ -353,11 +353,6 @@ func TestRunWithOverlappingExportTargetsFails(t *testing.T) { func TestRunWithMultipleTargetsIgnoresSymlinkExport(t *testing.T) { defer os.Chdir(getWdOrFatal(t)) - oldExperiments := regolith.EnabledExperiments - regolith.EnabledExperiments = []string{"symlink_export"} - t.Cleanup(func() { - regolith.EnabledExperiments = oldExperiments - }) tmpDir := prepareTestDirectory( fmt.Sprintf("%s-%d", t.Name(), time.Now().UnixNano()), t) @@ -398,8 +393,8 @@ func TestRunWithMultipleTargetsIgnoresSymlinkExport(t *testing.T) { } os.Chdir(workingDir) - if err := regolith.Run("multi", []string{}, true, "", false); err != nil { - t.Fatal("Multi-target run with symlink_export enabled failed:", err) + if err := regolith.Run("multi", []string{}, true, "", false, true, false); err != nil { + t.Fatal("Multi-target run with --symlink-export enabled failed:", err) } for _, tmpPack := range []string{"BP", "RP"} { @@ -472,7 +467,7 @@ func TestRunWithLocalAndDevelopmentExportTargets(t *testing.T) { } os.Chdir(workingDir) - if err := regolith.Run("local_and_development", []string{}, false, "", false); err != nil { + if err := regolith.Run("local_and_development", []string{}, false, "", false, false, false); err != nil { t.Fatal("First mixed-target run failed:", err) } @@ -494,7 +489,7 @@ func TestRunWithLocalAndDevelopmentExportTargets(t *testing.T) { t, ) - if err := regolith.Run("local_and_development", []string{}, false, "", false); err != nil { + if err := regolith.Run("local_and_development", []string{}, false, "", false, false, false); err != nil { t.Fatal("Second mixed-target run failed safety checks:", err) } } diff --git a/test/file_protection_test.go b/test/file_protection_test.go index 1a281746..865a2594 100644 --- a/test/file_protection_test.go +++ b/test/file_protection_test.go @@ -34,18 +34,18 @@ func TestSwitchingExportTargets(t *testing.T) { // Run Regolith with targets: A, B, A t.Log("Testing the 'regolith run' with changing export targets...") t.Log("Running Regolith with target A...") - err := regolith.Run("exact_export_A", []string{}, true, "", false) + err := regolith.Run("exact_export_A", []string{}, true, "", false, false, false) if err != nil { t.Fatal( "Unable RunProfile failed on first attempt to export to A:", err) } t.Log("Running Regolith with target B...") - err = regolith.Run("exact_export_B", []string{}, true, "", false) + err = regolith.Run("exact_export_B", []string{}, true, "", false, false, false) if err != nil { t.Fatal("Unable RunProfile failed on attempt to export to B:", err) } t.Log("Running Regolith with target A (2nd time)...") - err = regolith.Run("exact_export_A", []string{}, true, "", false) + err = regolith.Run("exact_export_A", []string{}, true, "", false, false, false) if err != nil { t.Fatal( "Unable RunProfile failed on second attempt to export to A:", err) @@ -78,7 +78,7 @@ func TestTriggerFileProtection(t *testing.T) { // 1. Run Regolith (export to A) t.Log("Testing the 'regolith run' with file protection...") t.Log("Running Regolith...") - err := regolith.Run("exact_export_A", []string{}, true, "", false) + err := regolith.Run("exact_export_A", []string{}, true, "", false, false, false) if err != nil { t.Fatal( "Unable RunProfile failed on first attempt to export to A:", err) @@ -94,7 +94,7 @@ func TestTriggerFileProtection(t *testing.T) { // 3. Run Regolith (export to A), expect failure. t.Log("Running Regolith (this should be stopped by file protection system)...") - err = regolith.Run("exact_export_A", []string{}, true, "", false) + err = regolith.Run("exact_export_A", []string{}, true, "", false, false, false) if err == nil { t.Fatal("Expected RunProfile to fail on second attempt to export to A") } diff --git a/test/filter_async_test.go b/test/filter_async_test.go index 6ace4dbe..4bc746e8 100644 --- a/test/filter_async_test.go +++ b/test/filter_async_test.go @@ -30,7 +30,7 @@ func TestAsyncFilter(t *testing.T) { t.Log("Running Regolith with a conditional filter...") start := time.Now() - if err := regolith.Run("default", []string{}, true, "", false); err != nil { + if err := regolith.Run("default", []string{}, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } duration := time.Since(start) diff --git a/test/local_filters_test.go b/test/local_filters_test.go index 6f287e87..6eb17158 100644 --- a/test/local_filters_test.go +++ b/test/local_filters_test.go @@ -44,7 +44,7 @@ func TestRegolithRunMissingRp(t *testing.T) { os.Chdir(tmpDir) // THE TEST - err := regolith.Run("dev", []string{}, true, "", false) + err := regolith.Run("dev", []string{}, true, "", false, false, false) if err != nil { t.Fatal("'regolith run' failed:", err) } @@ -71,7 +71,7 @@ func TestLocalRequirementsInstallAndRun(t *testing.T) { t.Fatal("'regolith install-all' failed", err.Error()) } t.Log("Testing the 'regolith run' command...") - if err := regolith.Run("dev", []string{}, true, "", false); err != nil { + if err := regolith.Run("dev", []string{}, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } } @@ -95,7 +95,7 @@ func TestExeFilterRun(t *testing.T) { // THE TEST t.Log("Testing the 'regolith run' command...") - if err := regolith.Run("dev", []string{}, true, "", false); err != nil { + if err := regolith.Run("dev", []string{}, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } // TEST EVALUATION @@ -125,7 +125,7 @@ func TestProfileFilterRun(t *testing.T) { // THE TEST // Invalid profile (shoud fail) t.Log("Running invalid profile filter with circular dependencies (this should fail).") - err := regolith.Run("invalid_circular_profile_1", []string{}, true, "", false) + err := regolith.Run("invalid_circular_profile_1", []string{}, true, "", false, false, false) if err == nil { t.Fatal("'regolith run' didn't return an error after running" + " a circular profile filter.") @@ -134,7 +134,7 @@ func TestProfileFilterRun(t *testing.T) { } // Valid profile (should succeed) t.Log("Running valid profile filter.") - err = regolith.Run("correct_nested_profile", []string{}, true, "", false) + err = regolith.Run("correct_nested_profile", []string{}, true, "", false, false, false) if err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/pre_post_shell_os_specific_test.go b/test/pre_post_shell_os_specific_test.go index 1ea5a4d7..478a24ca 100644 --- a/test/pre_post_shell_os_specific_test.go +++ b/test/pre_post_shell_os_specific_test.go @@ -27,7 +27,7 @@ func TestPrePostShellCommandsOSSpecific(t *testing.T) { // THE TEST t.Log("Testing OS-specific preShell and postShell commands...") - if err := regolith.Run("default", nil, true, "", false); err != nil { + if err := regolith.Run("default", nil, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/pre_post_shell_test.go b/test/pre_post_shell_test.go index 2d619da4..8748f94c 100644 --- a/test/pre_post_shell_test.go +++ b/test/pre_post_shell_test.go @@ -27,7 +27,7 @@ func TestPrePostShellCommands(t *testing.T) { // THE TEST t.Log("Testing the 'regolith run' command with preShell and postShell...") - if err := regolith.Run("default", nil, true, "", false); err != nil { + if err := regolith.Run("default", nil, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } diff --git a/test/remote_filters_test.go b/test/remote_filters_test.go index 7699773d..e77d94b6 100644 --- a/test/remote_filters_test.go +++ b/test/remote_filters_test.go @@ -38,7 +38,7 @@ func TestInstallAllAndRun(t *testing.T) { } t.Log("Testing the 'regolith run' command...") - err = regolith.Run("dev", []string{}, true, "", false) + err = regolith.Run("dev", []string{}, true, "", false, false, false) if err != nil { t.Fatal("'regolith run' failed:", err) } @@ -78,7 +78,7 @@ func TestDataModifyRemoteFilter(t *testing.T) { } t.Log("Testing the 'regolith run' command...") - err = regolith.Run("default", []string{}, true, "", false) + err = regolith.Run("default", []string{}, true, "", false, false, false) if err != nil { t.Fatal("'regolith run' failed:", err) } diff --git a/test/size_time_check_optimization_method_test.go b/test/size_time_check_optimization_method_test.go index 059794e0..bffc879c 100644 --- a/test/size_time_check_optimization_method_test.go +++ b/test/size_time_check_optimization_method_test.go @@ -45,13 +45,12 @@ func TestSizeTimeCheckOptimizationCorectness(t *testing.T) { os.Chdir(tmpDir) // THE TEST - // Enable the experiment - regolith.EnabledExperiments = append(regolith.EnabledExperiments, "size_time_check") + // Size time check is enabled by default // Run the project t.Log("Running Regolith...") - if err := regolith.Run("default", []string{}, true, "", false); err != nil { + if err := regolith.Run("default", []string{}, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } // TEST EVALUATION @@ -83,8 +82,7 @@ func TestSizeTimeCheckOptimizationSpeed(t *testing.T) { os.Chdir(tmpDir) // THE TEST - // Enable the experiment - regolith.EnabledExperiments = append(regolith.EnabledExperiments, "size_time_check") + // Size time check is enabled by default // Run the project twice, the second run should be faster runtimes := make([]time.Duration, 0) @@ -94,7 +92,7 @@ func TestSizeTimeCheckOptimizationSpeed(t *testing.T) { // Start the timer start := time.Now() - if err := regolith.Run("default", []string{}, true, "", false); err != nil { + if err := regolith.Run("default", []string{}, true, "", false, false, false); err != nil { t.Fatal("'regolith run' failed:", err.Error()) } // Stop the timer From cb5fec9ad639607a2f55e812b19a67c732775d65 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 14 May 2026 00:09:46 +0200 Subject: [PATCH 25/31] AI commit: Use 'unified' version of the $schema instaead of a specific version, make sure Regolith supports version formatVerison 1.8.0. --- regolith/config.go | 2 +- regolith/export.go | 2 +- regolith/main_functions.go | 4 ++-- test/testdata/fresh_project/config.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/regolith/config.go b/regolith/config.go index 706b09f2..0a35c4e4 100644 --- a/regolith/config.go +++ b/regolith/config.go @@ -8,7 +8,7 @@ import ( "golang.org/x/mod/semver" ) -const latestCompatibleVersion = "1.7.0" +const latestCompatibleVersion = "1.8.0" const StandardLibraryUrl = "github.com/Bedrock-OSS/regolith-filters" const ConfigFilePath = "config.json" diff --git a/regolith/export.go b/regolith/export.go index 23a687a9..ac20061f 100644 --- a/regolith/export.go +++ b/regolith/export.go @@ -29,7 +29,7 @@ func GetExportPaths( if semver.Compare(vFormatVersion, "v1.4.0") < 0 { bpPath, rpPath, err = getExportPathsV1_2_0( exportTarget, bpName, rpName) - } else if semver.Compare(vFormatVersion, "v1.7.0") <= 0 { + } else if semver.Compare(vFormatVersion, "v1.8.0") <= 0 { bpPath, rpPath, err = getExportPathsV1_4_0( exportTarget, bpName, rpName) } else { diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 3c162b43..c0c8e2f4 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -480,7 +480,7 @@ func Init(debug, force bool, env string) error { ResourceFolder: "./packs/RP", }, RegolithProject: RegolithProject{ - FormatVersion: "1.7.0", + FormatVersion: "1.8.0", DataPath: "./packs/data", FilterDefinitions: map[string]FilterInstaller{}, Profiles: map[string]Profile{ @@ -503,7 +503,7 @@ func Init(debug, force bool, env string) error { // Add the schema property, this is a little hacky rawJsonData := make(map[string]any, 0) json.Unmarshal(jsonBytes, &rawJsonData) - rawJsonData["$schema"] = "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.7.json" + rawJsonData["$schema"] = "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/unified.json" jsonBytes, _ = json.MarshalIndent(rawJsonData, "", "\t") err = os.WriteFile(ConfigFilePath, jsonBytes, 0644) diff --git a/test/testdata/fresh_project/config.json b/test/testdata/fresh_project/config.json index f1d45e69..aa30c009 100644 --- a/test/testdata/fresh_project/config.json +++ b/test/testdata/fresh_project/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.7.json", + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/unified.json", "author": "Your name", "name": "Project name", "packs": { @@ -9,7 +9,7 @@ "regolith": { "dataPath": "./packs/data", "filterDefinitions": {}, - "formatVersion": "1.7.0", + "formatVersion": "1.8.0", "profiles": { "default": { "export": { From 04a98487d2fad4ba58b92e1ec6a7f479d9509432 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 14 May 2026 08:54:29 +0200 Subject: [PATCH 26/31] Disabled the symlink_export feature in the TestInstallAndRun() test to prevent test failures caused by the additional files left in the tmp folder. The original test was designed with the size_time_check function disabled. --- test/remote_filters_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/remote_filters_test.go b/test/remote_filters_test.go index e77d94b6..06a9afdd 100644 --- a/test/remote_filters_test.go +++ b/test/remote_filters_test.go @@ -38,7 +38,7 @@ func TestInstallAllAndRun(t *testing.T) { } t.Log("Testing the 'regolith run' command...") - err = regolith.Run("dev", []string{}, true, "", false, false, false) + err = regolith.Run("dev", []string{}, true, "", false, false, true) if err != nil { t.Fatal("'regolith run' failed:", err) } From 5692eb4cf836b9a18c342b475afa97b4c231fd34 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 14 May 2026 09:12:13 +0200 Subject: [PATCH 27/31] Moved repeated of the messages in --help into variables. --- main.go | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index a5d4c0a1..51f23f59 100644 --- a/main.go +++ b/main.go @@ -227,6 +227,7 @@ func main() { rootCmd.PersistentFlags().StringVar(&envFile, "env", "", "Path to a custom .env file to load") var force bool + forceDesc := "Force the operation, overriding potential safeguards." // regolith init cmdInit := &cobra.Command{ Use: "init", @@ -238,10 +239,13 @@ func main() { }, } cmdInit.Flags().BoolVarP( - &force, "force", "f", false, "Force the operation, overriding potential safeguards.") + &force, "force", "f", false, forceDesc) subcommands = append(subcommands, cmdInit) profiles := []string{} + // Messages for common flags in 'regolith install' and 'regolith install-all' + forceFilterRefreshDesc := "Force filter cache refresh." + // regolith install var update, resolverRefresh, filterRefresh bool cmdInstall := &cobra.Command{ @@ -261,11 +265,11 @@ func main() { }, } cmdInstall.Flags().BoolVarP( - &force, "force", "f", false, "Force the operation, overriding potential safeguards.") + &force, "force", "f", false, forceDesc) cmdInstall.Flags().BoolVar( &resolverRefresh, "force-resolver-refresh", false, "Force resolvers refresh.") cmdInstall.Flags().BoolVar( - &filterRefresh, "force-filter-refresh", false, "Force filter cache refresh.") + &filterRefresh, "force-filter-refresh", false, forceFilterRefreshDesc) cmdInstall.Flags().BoolVarP( &force, "update", "u", false, "An alias for --force flag. Use this flag to update filters.") cmdInstall.Flags().StringSliceVarP(&profiles, "profile", "p", profiles, "Adds installed filters to the specified profiles. If no profile is provided, the filter will be added to the default profile.") @@ -283,13 +287,18 @@ func main() { }, } cmdInstallAll.Flags().BoolVarP( - &force, "force", "f", false, "Force the operation, overriding potential safeguards.") + &force, "force", "f", false, forceDesc) cmdInstallAll.Flags().BoolVarP( &update, "update", "u", false, "Updates the remote filters to the latest stable version available.") cmdInstallAll.Flags().BoolVar( - &filterRefresh, "force-filter-refresh", false, "Force filter cache refresh.") + &filterRefresh, "force-filter-refresh", false, forceFilterRefreshDesc) subcommands = append(subcommands, cmdInstallAll) + // Messages for common flags in 'regolith run' and 'regolith watch' + unsafeDesc := "Disables file protection safety checks for faster exports." + symlinkExportDesc := "Creates links from the tmp directory to the export target so that files written to tmp are immediately reflected in the export location." + disableSizeTimeCheckDesc := "Disables the size and modification time check optimization for file exporting." + // regolith run var symlinkExport, disableSizeTimeCheck bool cmdRun := &cobra.Command{ @@ -310,9 +319,9 @@ func main() { err = regolith.Run(profile, extraFilterArgs, burrito.PrintStackTrace, env, unsafe, symlink, disableStc) }, } - cmdRun.Flags().Bool("unsafe", false, "Disables file protection safety checks for faster exports") - cmdRun.Flags().BoolVar(&symlinkExport, "symlink-export", false, "Creates links from the tmp directory to the export target so that files written to tmp are immediately reflected in the export location.") - cmdRun.Flags().BoolVar(&disableSizeTimeCheck, "disable-size-time-check", false, "Disables the size and modification time check optimization for file exporting.") + cmdRun.Flags().Bool("unsafe", false, unsafeDesc) + cmdRun.Flags().BoolVar(&symlinkExport, "symlink-export", false, symlinkExportDesc) + cmdRun.Flags().BoolVar(&disableSizeTimeCheck, "disable-size-time-check", false, disableSizeTimeCheckDesc) subcommands = append(subcommands, cmdRun) // regolith watch @@ -334,9 +343,9 @@ func main() { err = regolith.Watch(profile, extraFilterArgs, burrito.PrintStackTrace, env, unsafe, symlink, disableStc) }, } - cmdWatch.Flags().Bool("unsafe", false, "Disables file protection safety checks for faster exports") - cmdWatch.Flags().BoolVar(&symlinkExport, "symlink-export", false, "Creates links from the tmp directory to the export target so that files written to tmp are immediately reflected in the export location.") - cmdWatch.Flags().BoolVar(&disableSizeTimeCheck, "disable-size-time-check", false, "Disables the size and modification time check optimization for file exporting.") + cmdWatch.Flags().Bool("unsafe", false, unsafeDesc) + cmdWatch.Flags().BoolVar(&symlinkExport, "symlink-export", false, symlinkExportDesc) + cmdWatch.Flags().BoolVar(&disableSizeTimeCheck, "disable-size-time-check", false, disableSizeTimeCheckDesc) subcommands = append(subcommands, cmdWatch) // regolith apply-filter From f1c12471c6cee0d4993544ae1d41d4f5d56ba303 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 14 May 2026 09:14:11 +0200 Subject: [PATCH 28/31] Instead of removing the code related to experiments, leave it as a comment. --- main.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/main.go b/main.go index 51f23f59..7a243659 100644 --- a/main.go +++ b/main.go @@ -417,9 +417,20 @@ func main() { } subcommands = append(subcommands, cmdUpdateResolvers) + // // Generate the description for the experiments + // experimentDescs := make([]string, len(regolith.AvailableExperiments)) + // for i, experiment := range regolith.AvailableExperiments { + // experimentDescs[i] = "- " + experiment.Name + " - " + strings.Trim(experiment.Description, "\n") + // } + + // add --debug, --timings and --experiment flag to every command for _, cmd := range subcommands { cmd.Flags().BoolVarP(&burrito.PrintStackTrace, "debug", "", false, "Enables debugging") cmd.Flags().BoolVarP(®olith.EnableTimings, "timings", "", false, "Enables timing information") + // cmd.Flags().StringSliceVar( + // ®olith.EnabledExperiments, "experiments", nil, + // "Enables experimental features. Currently supported experiments:\n"+ + // strings.Join(experimentDescs, "\n")) } // Build and run CLI From 54b9512f5b4d046691cb3f5c255f07d454c75096 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 14 May 2026 09:36:13 +0200 Subject: [PATCH 29/31] Removed unnecessary special case for copying files into the tmp directory when symlink export is enabled. --- regolith/profile.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/regolith/profile.go b/regolith/profile.go index 3ee89241..fa73cfb9 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -243,14 +243,6 @@ func SetupTmpFiles(context RunContext) error { if err != nil { return burrito.WrapError(err, "Failed to export behavior pack.") } - } else if useSymlinkExport { - err = copy.Copy( - path, - p, - copy.Options{PreserveTimes: false, Sync: true}) - if err != nil { - return burrito.WrapErrorf(err, osCopyError, path, p) - } } else { err = copy.Copy( path, From 8d1df6e69be49fefbd8a39258bf235d8b14beea9 Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 14 May 2026 10:25:17 +0200 Subject: [PATCH 30/31] Updated the data of tests failing due to the changes made to the config.json file. --- test/testdata/regolith_install/1.0.0/config.json | 4 ++-- test/testdata/regolith_install/1.0.1/config.json | 4 ++-- test/testdata/regolith_install/HEAD/config.json | 4 ++-- test/testdata/regolith_install/latest/config.json | 4 ++-- test/testdata/regolith_install/sha/config.json | 4 ++-- test/testdata/regolith_install/tag/config.json | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/testdata/regolith_install/1.0.0/config.json b/test/testdata/regolith_install/1.0.0/config.json index eaff1153..eeb8bb01 100644 --- a/test/testdata/regolith_install/1.0.0/config.json +++ b/test/testdata/regolith_install/1.0.0/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.7.json", + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/unified.json", "author": "Your name", "name": "Project name", "packs": { @@ -14,7 +14,7 @@ "version": "1.0.0" } }, - "formatVersion": "1.7.0", + "formatVersion": "1.8.0", "profiles": { "default": { "export": { diff --git a/test/testdata/regolith_install/1.0.1/config.json b/test/testdata/regolith_install/1.0.1/config.json index 6044653b..1cfa93a8 100644 --- a/test/testdata/regolith_install/1.0.1/config.json +++ b/test/testdata/regolith_install/1.0.1/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.7.json", + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/unified.json", "author": "Your name", "name": "Project name", "packs": { @@ -14,7 +14,7 @@ "version": "1.0.1" } }, - "formatVersion": "1.7.0", + "formatVersion": "1.8.0", "profiles": { "default": { "export": { diff --git a/test/testdata/regolith_install/HEAD/config.json b/test/testdata/regolith_install/HEAD/config.json index 2e0b328f..26f0ee2c 100644 --- a/test/testdata/regolith_install/HEAD/config.json +++ b/test/testdata/regolith_install/HEAD/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.7.json", + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/unified.json", "author": "Your name", "name": "Project name", "packs": { @@ -14,7 +14,7 @@ "version": "HEAD" } }, - "formatVersion": "1.7.0", + "formatVersion": "1.8.0", "profiles": { "default": { "export": { diff --git a/test/testdata/regolith_install/latest/config.json b/test/testdata/regolith_install/latest/config.json index 9f3ac789..ec533988 100644 --- a/test/testdata/regolith_install/latest/config.json +++ b/test/testdata/regolith_install/latest/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.7.json", + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/unified.json", "author": "Your name", "name": "Project name", "packs": { @@ -14,7 +14,7 @@ "version": "latest" } }, - "formatVersion": "1.7.0", + "formatVersion": "1.8.0", "profiles": { "default": { "export": { diff --git a/test/testdata/regolith_install/sha/config.json b/test/testdata/regolith_install/sha/config.json index a3ad8ca8..65177bb8 100644 --- a/test/testdata/regolith_install/sha/config.json +++ b/test/testdata/regolith_install/sha/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.7.json", + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/unified.json", "author": "Your name", "name": "Project name", "packs": { @@ -14,7 +14,7 @@ "version": "0c129227eb90e2f10a038755e4756fdd47e765e6" } }, - "formatVersion": "1.7.0", + "formatVersion": "1.8.0", "profiles": { "default": { "export": { diff --git a/test/testdata/regolith_install/tag/config.json b/test/testdata/regolith_install/tag/config.json index 637c5812..7b368191 100644 --- a/test/testdata/regolith_install/tag/config.json +++ b/test/testdata/regolith_install/tag/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.7.json", + "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/unified.json", "author": "Your name", "name": "Project name", "packs": { @@ -14,7 +14,7 @@ "version": "TEST_TAG_1" } }, - "formatVersion": "1.7.0", + "formatVersion": "1.8.0", "profiles": { "default": { "export": { From 0feb26be32d97e0f66490dea63bff18439aaa79c Mon Sep 17 00:00:00 2001 From: Nusiq Date: Thu, 14 May 2026 20:35:07 +0200 Subject: [PATCH 31/31] Fixed the node_runner_override selecting incorrect runner for the remote filters. --- regolith/config.go | 2 +- regolith/filter.go | 9 +++++++-- regolith/filter_remote.go | 2 +- regolith/profile.go | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/regolith/config.go b/regolith/config.go index 0a35c4e4..2eaf6856 100644 --- a/regolith/config.go +++ b/regolith/config.go @@ -208,7 +208,7 @@ func RegolithProjectFromObject( "object") } filterInstaller, err := FilterInstallerFromObject( - filterDefinitionName, filterDefinitionMap) + filterDefinitionName, filterDefinitionName, filterDefinitionMap) if err != nil { return result, burrito.WrapErrorf( err, jsonPropertyParseError, "filterDefinitions") diff --git a/regolith/filter.go b/regolith/filter.go index 41588686..3fc88b8f 100644 --- a/regolith/filter.go +++ b/regolith/filter.go @@ -338,7 +338,12 @@ var filterInstallerFactories = map[string]filterInstallerFactory{ }, } -func FilterInstallerFromObject(id string, obj map[string]any) (FilterInstaller, error) { +// FilterInstallerFromObject creates a FilterInstaller from a JSON object. +// +// It uses id as the filter ID, rootId as the remote root ID when applicable, +// and obj as the JSON definition of the filter. The rootId should be the same +// as id if the filter is not a remote filter. +func FilterInstallerFromObject(id, rootId string, obj map[string]any) (FilterInstaller, error) { runWith, _ := obj["runWith"].(string) if runWith == "nodejs" { userConfig, err := getCombinedUserConfig() @@ -350,7 +355,7 @@ func FilterInstallerFromObject(id string, obj map[string]any) (FilterInstaller, if ok { runWith = defaultOverride } - override, ok := userConfig.NodeRunnerOverride[id] + override, ok := userConfig.NodeRunnerOverride[rootId] if ok { runWith = override } diff --git a/regolith/filter_remote.go b/regolith/filter_remote.go index 2e60af18..25690d04 100644 --- a/regolith/filter_remote.go +++ b/regolith/filter_remote.go @@ -181,7 +181,7 @@ func (f *RemoteFilterDefinition) InstallDependencies(_ *RemoteFilterDefinition, "Filter: %s", f.Id) } filterInstaller, err := FilterInstallerFromObject( - fmt.Sprintf("%v:subfilter%v", f.Id, i), filter) + fmt.Sprintf("%v:subfilter%v", f.Id, i), f.Id, filter) if err != nil { return extraFilterJsonErrorInfo( path, burrito.WrapErrorf(err, jsonPathParseError, jsonPath)) diff --git a/regolith/profile.go b/regolith/profile.go index fa73cfb9..ea126899 100644 --- a/regolith/profile.go +++ b/regolith/profile.go @@ -473,7 +473,7 @@ func (f *RemoteFilter) subfilterCollection(dotRegolithPath string) (*FilterColle // Using the same JSON data to create both the filter // definition (installer) and the filter (runner) filterId := fmt.Sprintf("%v:subfilter%v", f.Id, i) - filterInstaller, err := FilterInstallerFromObject(filterId, filter) + filterInstaller, err := FilterInstallerFromObject(filterId, f.Id, filter) if err != nil { return nil, extraFilterJsonErrorInfo( path, burrito.WrapErrorf(err, jsonPathParseError, jsonPath))