diff --git a/docs/docs/guide/getting-started.md b/docs/docs/guide/getting-started.md index 89f9f4a8..32f99357 100644 --- a/docs/docs/guide/getting-started.md +++ b/docs/docs/guide/getting-started.md @@ -131,11 +131,13 @@ Now, you can re-run `regolith run`. Check `com.mojang`, and open the new `texture_list.json` file in `RP/textures/texture_list.json`. Every time you run regolith, this file will be re-created, based on your current textures. No need to manually edit it ever again! -{: .notice--warning} +:::: warning `Warning:` If your resource pack already contains `texture_list.json`, you should delete it. You don't need to manually worry about it anymore - Regolith will handle it! +:::: -{: .notice--warning} +:::: warning `Warning:` If your project doesn't have any textures, than `texture_list.json` will simply create a blank file `[]`. Consider adding some textures to see the filter at work! +:::: ## Whats Next diff --git a/docs/docs/guide/installing-filters.md b/docs/docs/guide/installing-filters.md index 2152d8af..4a6535c5 100644 --- a/docs/docs/guide/installing-filters.md +++ b/docs/docs/guide/installing-filters.md @@ -65,15 +65,17 @@ Regolith is intended to be used with git version control, and by default the `.r You may use the command `regolith install-all`, which will check `config.json`, and install every filter in the `filterDefinitions`. -{: .notice--warning} +:::: warning This is only intended to be used with existing projects. To install new filters, use `regolith install`. +:::: ## Filter Versioning Filters in Regolith are optionally versioned with a [semantic version](https://semver.org/). As filters get updated, new versions will be released, and you can optionally update. -{: .notice--warning} +:::: warning If you don't specify a version, the `install` command will pick a sensible default. First, it will search for the latest release. If that doesn't exist (such as a filter that has no versions), it will select the latest commit in the repository. In both cases, the installed version will be `pinned`. +:::: ### Installing a Specific Version @@ -102,6 +104,12 @@ regolith install name_ninja --update Alternatively, you can modify the `version` field in `config.json` and run `regolith install-all`. Regolith install-all is useful for working in a team, when other team members may have to update or add filters to the project. +If you want to update all filters in your project, you can use the `--update` flag with the `install-all` command. + +``` +regolith install-all --update +``` + ### Updating resolvers When using short names for filters, Regolith uses a resolver file from a remote repository to determine the URL of the filter. diff --git a/main.go b/main.go index 254ed3f0..17b2403b 100644 --- a/main.go +++ b/main.go @@ -221,7 +221,7 @@ func main() { &force, "force", "f", false, "Force the operation, overriding potential safeguards.") subcommands = append(subcommands, cmdInit) - profiles := []string{"default"} + profiles := []string{} // regolith install var update, resolverRefresh, filterRefresh bool cmdInstall := &cobra.Command{ @@ -233,7 +233,10 @@ func main() { cmd.Help() return } - err = regolith.Install(filters, force || update, resolverRefresh, filterRefresh, cmd.Flags().Lookup("profile").Changed, profiles, burrito.PrintStackTrace) + if cmd.Flags().Lookup("profile").Changed && len(profiles) == 0 { + profiles = append(profiles, "default") + } + err = regolith.Install(filters, force || update, resolverRefresh, filterRefresh, profiles, burrito.PrintStackTrace) }, } cmdInstall.Flags().BoolVarP( @@ -254,11 +257,13 @@ func main() { Short: "Installs all nonexistent or outdated filters defined in filterDefinitions list", Long: regolithInstallAllDesc, Run: func(cmd *cobra.Command, _ []string) { - err = regolith.InstallAll(force, burrito.PrintStackTrace, filterRefresh) + err = regolith.InstallAll(force, update, burrito.PrintStackTrace, filterRefresh) }, } cmdInstallAll.Flags().BoolVarP( &force, "force", "f", false, "Force the operation, overriding potential safeguards.") + 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.") subcommands = append(subcommands, cmdInstallAll) diff --git a/regolith/filter.go b/regolith/filter.go index d5c802c7..49fbab06 100644 --- a/regolith/filter.go +++ b/regolith/filter.go @@ -57,18 +57,7 @@ func (c *RunContext) StartWatchingSourceFiles() error { if c.interruptionChannel != nil { return burrito.WrappedError("Files are already being watched.") } - rpWatcher, err := NewDirWatcher(c.Config.ResourceFolder) - if err != nil { - return burrito.WrapError(err, "Could not create resource pack watcher.") - } - bpWatcher, err := NewDirWatcher(c.Config.BehaviorFolder) - if err != nil { - return burrito.WrapError(err, "Could not create behavior pack watcher.") - } - dataWatcher, err := NewDirWatcher(c.Config.DataPath) - if err != nil { - return burrito.WrapError(err, "Could not create data watcher.") - } + c.interruptionChannel = make(chan string) yieldChanges := func( watcher *DirWatcher, sourceName string, @@ -81,16 +70,36 @@ func (c *RunContext) StartWatchingSourceFiles() error { } } } - go yieldChanges(rpWatcher, "rp") - go yieldChanges(bpWatcher, "bp") - go yieldChanges(dataWatcher, "data") - return nil -} -// AwaitInterruption locks the goroutine with the interruption channel until -// the Config is interrupted and returns the interruption message. -func (c *RunContext) AwaitInterruption() string { - return <-c.interruptionChannel + addWatcher := func(watchedPath, watcherString string) error { + watcher, err := NewDirWatcher(watchedPath) + if err != nil { + return burrito.PassError(err) + } + go yieldChanges(watcher, watcherString) + return nil + } + + var err error + if c.Config.ResourceFolder != "" { + err = addWatcher(c.Config.ResourceFolder, "rp") + if err != nil { + return burrito.WrapError(err, "Could not create resource pack watcher.") + } + } + if c.Config.BehaviorFolder != "" { + err = addWatcher(c.Config.BehaviorFolder, "bp") + if err != nil { + return burrito.WrapError(err, "Could not create behavior pack watcher.") + } + } + if c.Config.DataPath != "" { + err = addWatcher(c.Config.DataPath, "data") + if err != nil { + return burrito.WrapError(err, "Could not create data watcher.") + } + } + return nil } // IsInterrupted returns true if there is a message on the interruptionChannel diff --git a/regolith/filter_remote.go b/regolith/filter_remote.go index 7c7943bc..66bfeb14 100644 --- a/regolith/filter_remote.go +++ b/regolith/filter_remote.go @@ -342,8 +342,8 @@ func FilterDefinitionFromTheInternet( if version == "" { // "" locks the version to the latest version, err = GetRemoteFilterDownloadRef(url, name, version) if err != nil { - return nil, burrito.WrappedErrorf( - getRemoteFilterDownloadRefError, url, name, version) + return nil, burrito.WrapErrorf( + err, getRemoteFilterDownloadRefError, url, name, version) } version = trimFilterPrefix(version, name) } @@ -547,10 +547,10 @@ func (f *RemoteFilterDefinition) InstalledVersion(dotRegolithPath string) (strin return versionStr, nil } -func (f *RemoteFilterDefinition) Update(force bool, dotRegolithPath string, isInstall, refreshFilters bool) error { +func (f *RemoteFilterDefinition) Update(force bool, dotRegolithPath, dataPath string, refreshFilters bool) error { installedVersion, err := f.InstalledVersion(dotRegolithPath) installedVersion = trimFilterPrefix(installedVersion, f.Id) - if err != nil && (!isInstall || force) { + if err != nil && force { Logger.Warnf("Unable to get installed version of filter %q.", f.Id) } MeasureStart("Get remote filter download ref") @@ -569,6 +569,8 @@ func (f *RemoteFilterDefinition) Update(force bool, dotRegolithPath string, isIn if err != nil { return burrito.PassError(err) } + // Copy the data of the remote filter to the data path + f.CopyFilterData(dataPath, dotRegolithPath) err = f.InstallDependencies(f, dotRegolithPath) if err != nil { return burrito.PassError(err) diff --git a/regolith/install_add.go b/regolith/install_add.go index 213c1acb..3127f690 100644 --- a/regolith/install_add.go +++ b/regolith/install_add.go @@ -1,6 +1,7 @@ package regolith import ( + "encoding/json" "os" "os/exec" "path/filepath" @@ -28,8 +29,8 @@ type parsedInstallFilterArg struct { // and copies their data to the data path. If the filter is already installed, // it returns an error unless the force flag is set. func installFilters( - filterDefinitions map[string]FilterInstaller, force bool, - dataPath, dotRegolithPath string, isInstall, refreshFilters bool, + filtersToInstall map[string]FilterInstaller, force bool, + dataPath, dotRegolithPath string, refreshFilters bool, ) error { joinedPath := filepath.Join(dotRegolithPath, "cache/filters") err := os.MkdirAll(joinedPath, 0755) @@ -43,16 +44,14 @@ func installFilters( } // Download all the remote filters - for name, filterDefinition := range filterDefinitions { + for name, filterDefinition := range filtersToInstall { Logger.Infof("Downloading %q filter...", name) if remoteFilter, ok := filterDefinition.(*RemoteFilterDefinition); ok { // Download the remote filter, and its dependencies - err := remoteFilter.Update(force, dotRegolithPath, isInstall, refreshFilters) + err := remoteFilter.Update(force, dotRegolithPath, dataPath, refreshFilters) if err != nil { return burrito.WrapErrorf(err, remoteFilterDownloadError, name) } - // Copy the data of the remote filter to the data path - remoteFilter.CopyFilterData(dataPath, dotRegolithPath) } else { // Non-remote filters must always update their dependencies. // TODO - add option to track if the filter already installed @@ -70,6 +69,51 @@ func installFilters( return nil } +// addFiltersToConfig modifies the config by adding the specified filters +// to the list of the filters in the project. If the profiles list is not +// empty, it also adds the filters to the specified profiles. After modifying +// the config, it saves it to the standard config file location. +func addFiltersToConfig( + config map[string]interface{}, + filterInstallers map[string]FilterInstaller, + profiles []string, +) error { + filterDefinitions, err := filterDefinitionsFromConfigMap(config) + if err != nil { + return burrito.WrapError( + err, + "Failed to get the list of filter definitions from config file.") + } + // Add the filters to the config + for name, downloadedFilter := range filterInstallers { + // Add the filter to config file + filterDefinitions[name] = downloadedFilter + // Add the filter to the profile + for _, profile := range profiles { + profileMap, err := FindByJSONPath[map[string]interface{}](config, "regolith/profiles/"+EscapePathPart(profile)) + if err != nil { + return burrito.WrapErrorf( + err, "Profile %s does not exist or is invalid.", profile) + } + if profileMap["filters"] == nil { + profileMap["filters"] = make([]interface{}, 0) + } + // Add the filter to the profile + profileMap["filters"] = append( + profileMap["filters"].([]interface{}), map[string]interface{}{ + "filter": name, + }) + } + } + // Save the config file + jsonBytes, _ := json.MarshalIndent(config, "", "\t") + err = os.WriteFile(ConfigFilePath, jsonBytes, 0644) + if err != nil { + return burrito.WrapErrorf(err, fileWriteError, ConfigFilePath) + } + return nil +} + // parseInstallFilterArgs parses a list of arguments of the // "regolith install" command and returns a list of download tasks. func parseInstallFilterArgs( diff --git a/regolith/main_functions.go b/regolith/main_functions.go index 10922899..0e43d9db 100644 --- a/regolith/main_functions.go +++ b/regolith/main_functions.go @@ -4,9 +4,11 @@ import ( "encoding/json" "fmt" "os" + "os/signal" "path/filepath" "strconv" "strings" + "syscall" "time" "github.com/Bedrock-OSS/go-burrito/burrito" @@ -37,7 +39,7 @@ var disallowedFiles = []string{ // // The "debug" parameter is a boolean that determines if the debug messages // should be printed. -func Install(filters []string, force, refreshResolvers, refreshFilters, add bool, profiles []string, debug bool) error { +func Install(filters []string, force, refreshResolvers, refreshFilters bool, profiles []string, debug bool) error { InitLogging(debug) Logger.Info("Installing filters...") if !hasGit() { @@ -48,17 +50,11 @@ func Install(filters []string, force, refreshResolvers, refreshFilters, add bool return burrito.WrapError(err, "Unable to load config file.") } // Check if selected profiles exist - if add { - if len(profiles) == 0 { - profiles = []string{"default"} - } - // Get the profile - for _, profile := range profiles { - _, err := FindByJSONPath[map[string]interface{}](config, "regolith/profiles/"+EscapePathPart(profile)) - if err != nil { - return burrito.WrapErrorf( - err, "Profile %s does not exist or is invalid.", profile) - } + for _, profile := range profiles { + _, err := FindByJSONPath[map[string]interface{}](config, "regolith/profiles/"+EscapePathPart(profile)) + if err != nil { + return burrito.WrapErrorf( + err, "Profile %s does not exist or is invalid.", profile) } } // Get parts of config file required for installation @@ -110,13 +106,7 @@ func Install(filters []string, force, refreshResolvers, refreshFilters, add bool remoteFilterDefinition, err := FilterDefinitionFromTheInternet( parsedArg.url, parsedArg.name, parsedArg.version) if err != nil { - return burrito.WrapErrorf( - err, - "Unable to download the filter definition from the internet.\n"+ - "Filter repository Url: %s\n"+ - "Filter name: %s\n"+ - "Filter version: %s\n", - parsedArg.url, parsedArg.name, parsedArg.version) + return burrito.PassError(err) } if parsedArg.version == "HEAD" || parsedArg.version == "latest" { // The "HEAD" and "latest" keywords should be the same in the @@ -127,45 +117,21 @@ func Install(filters []string, force, refreshResolvers, refreshFilters, add bool } // Download the filter definitions err = installFilters( - filterInstallers, force, dataPath, dotRegolithPath, true, refreshFilters) + filterInstallers, force, dataPath, dotRegolithPath, refreshFilters) if err != nil { return burrito.WrapError(err, "Failed to install filters.") } - // Add the filters to the config - for name, downloadedFilter := range filterInstallers { - // Add the filter to config file - filterDefinitions[name] = downloadedFilter - if add { - // Add the filter to the profile - for _, profile := range profiles { - profileMap, err := FindByJSONPath[map[string]interface{}](config, "regolith/profiles/"+EscapePathPart(profile)) - // This check here is not necessary, because we have identical one at the beginning, but better to be safe - if err != nil { - return burrito.WrapErrorf( - err, "Profile %s does not exist or is invalid.", profile) - } - if profileMap["filters"] == nil { - profileMap["filters"] = make([]interface{}, 0) - } - // Add the filter to the profile - profileMap["filters"] = append( - profileMap["filters"].([]interface{}), map[string]interface{}{ - "filter": name, - }) - } - } - } - // Save the config file - jsonBytes, _ := json.MarshalIndent(config, "", "\t") - err = os.WriteFile(ConfigFilePath, jsonBytes, 0644) + + err = addFiltersToConfig(config, filterInstallers, profiles) if err != nil { return burrito.WrapErrorf( err, "Successfully downloaded %v filters"+ "but failed to update the config file.\n"+ "Run \"regolith clean\" to fix invalid cache state.", - len(parsedArgs)) + len(filterInstallers)) } + Logger.Info("Successfully installed the filters.") return sessionLockErr // Return the error from the defer function } @@ -179,7 +145,7 @@ func Install(filters []string, force, refreshResolvers, refreshFilters, add bool // // The "debug" parameter is a boolean that determines if the debug messages // should be printed. -func InstallAll(force, debug, refreshFilters bool) error { +func InstallAll(force, update, debug, refreshFilters bool) error { InitLogging(debug) Logger.Info("Installing filters...") if !hasGit() { @@ -202,21 +168,48 @@ func InstallAll(force, debug, refreshFilters bool) error { return burrito.WrapError(sessionLockErr, acquireSessionLockError) } defer func() { sessionLockErr = unlockSession() }() + + filtersToInstall := make(map[string]FilterInstaller, 0) + remoteFilters := make(map[string]FilterInstaller, 0) // Used for updating the config + if update { + for filterName, filterDefinition := range config.FilterDefinitions { + + switch fd := filterDefinition.(type) { + case *RemoteFilterDefinition: + filterInstaller, err := FilterDefinitionFromTheInternet( + fd.Url, fd.Id, "") + if err != nil { + return burrito.PassError(err) + } + filtersToInstall[filterName] = filterInstaller + remoteFilters[filterName] = filterInstaller + default: + filtersToInstall[filterName] = fd + } + } + } else { + filtersToInstall = config.FilterDefinitions + } // Install the filters err = installFilters( - config.FilterDefinitions, force, config.DataPath, dotRegolithPath, false, refreshFilters) + filtersToInstall, force, config.DataPath, dotRegolithPath, refreshFilters) if err != nil { return burrito.WrapError(err, "Could not install filters.") } + // Update the config + if update { + err = addFiltersToConfig(configMap, remoteFilters, nil) + if err != nil { + return burrito.WrapError(err, "Failed to update the config file.") + } + } Logger.Info("Successfully installed the filters.") return sessionLockErr // Return the error from the defer function } -// runOrWatch handles both 'regolith run' and 'regolith watch' commands based -// on the 'watch' parameter. It runs/watches the profile named after -// 'profileName' parameter. The 'debug' argument determines if the debug -// messages should be printed or not. -func runOrWatch(profileName string, debug, watch bool) error { +// prepareRunContext prepares the context for the "regolith run" and +// "regolith watch" commands. +func prepareRunContext(profileName string, debug, watch bool) (*RunContext, error) { InitLogging(debug) if profileName == "" { profileName = "default" @@ -224,65 +217,59 @@ func runOrWatch(profileName string, debug, watch bool) error { // Load the Config and the profile configJson, err := LoadConfigAsMap() if err != nil { - return burrito.WrapError(err, "Could not load \"config.json\".") + return nil, burrito.WrapError(err, "Could not load \"config.json\".") } config, err := ConfigFromObject(configJson) if err != nil { - return burrito.WrapError(err, "Could not load \"config.json\".") + return nil, burrito.WrapError(err, "Could not load \"config.json\".") } profile, ok := config.Profiles[profileName] if !ok { - return burrito.WrappedErrorf( + return nil, burrito.WrappedErrorf( "Profile %q does not exist in the configuration.", profileName) } // Get dotRegolithPath dotRegolithPath, err := GetDotRegolith(".") if err != nil { - return burrito.WrapError( + return nil, burrito.WrapError( err, "Unable to get the path to regolith cache folder.") } err = os.MkdirAll(dotRegolithPath, 0755) if err != nil { - return burrito.WrapErrorf(err, osMkdirError, dotRegolithPath) + return nil, burrito.WrapErrorf(err, osMkdirError, dotRegolithPath) } - // Lock the session - unlockSession, sessionLockErr := acquireSessionLock(dotRegolithPath) - if sessionLockErr != nil { - return burrito.WrapError(sessionLockErr, acquireSessionLockError) - } - defer func() { sessionLockErr = unlockSession() }() // Check the filters of the profile err = CheckProfileImpl(profile, profileName, *config, nil, dotRegolithPath) if err != nil { - return err + return nil, err } path, _ := filepath.Abs(".") - context := RunContext{ + return &RunContext{ AbsoluteLocation: path, Config: config, Parent: nil, Profile: profileName, DotRegolithPath: dotRegolithPath, Settings: map[string]interface{}{}, + }, 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, debug bool) error { + // Get the context + context, err := prepareRunContext(profileName, debug, false) + if err != nil { + return burrito.PassError(err) } - if watch { // Loop until program termination (CTRL+C) - context.StartWatchingSourceFiles() - for { - err = RunProfile(context) - if err != nil { - Logger.Errorf( - "Failed to run profile %q: %s", - profileName, burrito.PassError(err).Error()) - } else { - Logger.Infof("Successfully ran the %q profile.", profileName) - } - Logger.Info("Press Ctrl+C to stop watching.") - context.AwaitInterruption() - Logger.Warn("Restarting...") - } - // return nil // Unreachable code + // Lock the session + unlockSession, sessionLockErr := acquireSessionLock(context.DotRegolithPath) + if sessionLockErr != nil { + return burrito.WrapError(sessionLockErr, acquireSessionLockError) } - err = RunProfile(context) + defer func() { sessionLockErr = unlockSession() }() + // Run the profile + err = RunProfile(*context) if err != nil { return burrito.WrapErrorf(err, "Failed to run profile %q", profileName) } @@ -290,17 +277,46 @@ func runOrWatch(profileName string, debug, watch bool) error { return sessionLockErr // Return the error from the defer function } -// 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, debug bool) error { - return runOrWatch(profileName, debug, false) -} - // 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, debug bool) error { - return runOrWatch(profileName, debug, true) + // Get the context + context, err := prepareRunContext(profileName, debug, false) + if err != nil { + return burrito.PassError(err) + } + // Lock the session + unlockSession, sessionLockErr := acquireSessionLock(context.DotRegolithPath) + if sessionLockErr != nil { + return burrito.WrapError(sessionLockErr, acquireSessionLockError) + } + defer func() { sessionLockErr = unlockSession() }() + // Setup the channel for stopping the watching + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + // Run the profile + context.StartWatchingSourceFiles() + for { // Loop until program termination (CTRL+C) + err = RunProfile(*context) + if err != nil { + Logger.Errorf( + "Failed to run profile %q: %s", + profileName, burrito.PassError(err).Error()) + } else { + Logger.Infof("Successfully ran the %q profile.", profileName) + } + Logger.Info("Press Ctrl+C to stop watching.") + select { + case <-context.interruptionChannel: + // AwaitInterruption locks the goroutine with the interruption channel until + // the Config is interrupted and returns the interruption message. + Logger.Warn("Restarting...") + case <-sigChan: + return sessionLockErr // Return the error from the defer function + } + } } // ApplyFilter handles the "regolith apply-filter" command. diff --git a/test/local_filters_test.go b/test/local_filters_test.go index 8be38f22..32bd268a 100644 --- a/test/local_filters_test.go +++ b/test/local_filters_test.go @@ -66,7 +66,7 @@ func TestLocalRequirementsInstallAndRun(t *testing.T) { // THE TEST t.Log("Testing the 'regolith install-all' command...") - err := regolith.InstallAll(false, true, false) + err := regolith.InstallAll(false, false, true, false) if err != nil { t.Fatal("'regolith install-all' failed", err.Error()) } diff --git a/test/remote_filters_test.go b/test/remote_filters_test.go index ea677b8d..5acbf121 100644 --- a/test/remote_filters_test.go +++ b/test/remote_filters_test.go @@ -31,7 +31,7 @@ func TestInstallAllAndRun(t *testing.T) { // THE TEST t.Log("Testing the 'regolith install-all' command...") - err := regolith.InstallAll(false, true, false) + err := regolith.InstallAll(false, false, true, false) if err != nil { t.Fatal("'regolith install-all' failed:", err) } @@ -71,7 +71,7 @@ func TestDataModifyRemoteFilter(t *testing.T) { // THE TEST t.Log("Testing the 'regolith install-all' command...") - err := regolith.InstallAll(false, true, false) + err := regolith.InstallAll(false, false, true, false) if err != nil { t.Fatal("'regolith install-all' failed:", err) } @@ -127,8 +127,7 @@ func TestInstall(t *testing.T) { true, // Force false, // Refresh resolvers false, // Refresh filters - false, // Add to config - []string{"default"}, // Profiles + []string{}, // Profiles that should have the filter added true, // Debug ) if err != nil { @@ -187,7 +186,7 @@ func TestInstallAll(t *testing.T) { // Run 'regolith update' / 'regolith update-all' t.Log("Running 'regolith update'...") - err = regolith.InstallAll(false, true, false) + err = regolith.InstallAll(false, false, true, false) if err != nil { t.Fatal("'regolith update' failed:", err) }