diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f5fb3e..c11966e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.0] - 2023-02-01 + +### Added + +- Support for installing anything to any path. +- Prompt for confirmation when installing to a path that is not in working directory. + ## [0.6.0] - 2023-01-31 ### Added @@ -108,7 +115,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Basic functions: cache, install, list, show, tooth init, and uninstall. -[unreleased]: https://github.com/LiteLDev/Lip/compare/v0.6.0...HEAD +[unreleased]: https://github.com/LiteLDev/Lip/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/LiteLDev/Lip/releases/tag/v0.6.0...v0.7.0 [0.6.0]: https://github.com/LiteLDev/Lip/releases/tag/v0.5.1...v0.6.0 [0.5.1]: https://github.com/LiteLDev/Lip/releases/tag/v0.4.0...v0.5.1 [0.5.0]: https://github.com/LiteLDev/Lip/releases/tag/v0.4.0...v0.5.0 diff --git a/docs/en/commands/lip_install.md b/docs/en/commands/lip_install.md index c96a940..169b5fc 100644 --- a/docs/en/commands/lip_install.md +++ b/docs/en/commands/lip_install.md @@ -67,6 +67,10 @@ You can install any pre-release versions by specifying the version. And teeth ca Reinstall the tooth even if they are already up-to-date. When reinstalling, Lip will first uninstall the tooth and then install it. If version specified, Lip will install the version, otherwise the newest version. +- `-y, --yes` + + Assume yes to all prompts and run non-interactively. + ## Examples Install from tooth repositories: diff --git a/docs/en/tooth_json_file_reference.md b/docs/en/tooth_json_file_reference.md index 7202fe5..3a9d7ff 100644 --- a/docs/en/tooth_json_file_reference.md +++ b/docs/en/tooth_json_file_reference.md @@ -271,12 +271,10 @@ Indicates how should Lip handle file placement. When installing, files from "sou ### Syntax -Each placement rule should contain a source field and a destination field. Lip will extract files from the path relative to the root of the tooth specified by source and place them to the path relative to the root of BDS specified by destination. +Each placement rule should contain a source field and a destination field. Lip will extract files from the path relative to the root of the tooth specified by source and place them to the path specified by destination. If both the source and the destination ends with "*", the placement will be regarded as a wildcard. Lip will recursively place all files under the source directory to the destination directory. -Here we make a strict rule that the source and destination can only contain letters, digits, hyphens, underscores, dots, slashes and asterisks (for the last letter treated as wildcard) [a-zA-Z0-9-_\.\/\*]. If you want to place files to the root of BDS, you should specify every file in the source field. The first letter should not be a slash or a dot. The last letter should not be a slash. - You can also specify GOOS and GOARCH to optionally place files for specific platforms. For example, you can specify "windows" and "amd64" to place files only for Windows 64-bit. If you want to place files for all platforms, you can omit the GOOS and GOARCH fields. However, if you have specified GOARCH, you must also specify GOOS. ### Examples @@ -305,17 +303,13 @@ Extract from specific folders and place to specific folders: } ``` -### Notes - -Do not add any prefix like "/", "./" or "../". Otherwise, Lip will refused to install the tooth. - ## possession Declares the which folders are in the possession of the tooth. When uninstalling, files in the declared folders will be removed. However, when upgrading or reinstalling, Lip will keep files in both the possession of the previous version and the version to install (but those dedicated in placement will still be removed). ### Syntax -Each item of the list should be a valid directory path relative to the root of BDS ending with "/". +Each item of the list should be a valid directory path ending with "/". ### Examples @@ -439,12 +433,10 @@ This is a JSON schema of tooth.json, describing the syntax of tooth.json. ], "properties": { "source": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_]([a-zA-Z0-9-_\\.\/]*([a-zA-Z0-9-_]|\\/\\*))?$" + "type": "string" }, "destination": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_]([a-zA-Z0-9-_\\.\/]*([a-zA-Z0-9-_]|\\/\\*))?$" + "type": "string" }, "GOOS": { "type": "string" @@ -459,8 +451,7 @@ This is a JSON schema of tooth.json, describing the syntax of tooth.json. "type": "array", "additionalItems": false, "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_][a-zA-Z0-9-_\\.\/]*\\/$" + "type": "string" } }, "commands": { diff --git a/docs/zh-Hans/commands/lip_install.md b/docs/zh-Hans/commands/lip_install.md index 87708fc..9d8a13e 100644 --- a/docs/zh-Hans/commands/lip_install.md +++ b/docs/zh-Hans/commands/lip_install.md @@ -67,6 +67,10 @@ Lip在安装依赖之前,是按照 "拓扑顺序 "安装依赖。当遇到依 重新安装tooth包,即使它们已经是最新的了。重新安装时,Lip会先卸载已安装的tooth包,然后再安装它。如果指定了版本,Lip将安装该版本,否则就是最新的版本。 +- `-y, --yes` + + Assume yes to all prompts and run non-interactively. + ## 样例 从tooth存储库安装。 diff --git a/docs/zh-Hans/tooth_json_file_reference.md b/docs/zh-Hans/tooth_json_file_reference.md index aa3e119..6b277c9 100644 --- a/docs/zh-Hans/tooth_json_file_reference.md +++ b/docs/zh-Hans/tooth_json_file_reference.md @@ -272,12 +272,10 @@ Lip提供了一些版本匹配规则: ### 语法 -每个放置规则应该包含一个源字段和一个目标字段。Lip将从源字段指定的tooth包中的相对路径中提取文件,并将它们放置到目标地指定的BDS根目录的相对路径中。 +每个放置规则应该包含一个源字段和一个目标字段。Lip将从源字段指定的tooth包中的相对路径中提取文件,并将它们放置到目标地指定的路径中。 如果源目录和目标目录都以 "*"结尾,则该位置将被视为通配符。Lip将递归地把源目录下的所有文件放置到目标目录。 -在这里我们做了一个严格的规定,源和目的地只能包含字母、数字、连字符、下划线、点、斜线和星号(对于最后一个字母作为通配符处理)[a-zA-Z0-9-_\.\/\*]。如果你想把文件放到BDS的根部,你应该在`placement`字段中指定每个文件。第一个字母不应该是斜线或点。最后一个字母不应该是斜线。 - You can also specify GOOS and GOARCH to optionally place files for specific platforms. For example, you can specify "windows" and "amd64" to place files only for Windows 64-bit. If you want to place files for all platforms, you can omit the GOOS and GOARCH fields. However, if you have specified GOARCH, you must also specify GOOS. ### 样例 @@ -306,17 +304,13 @@ You can also specify GOOS and GOARCH to optionally place files for specific plat } ``` -### 注意 - -不要添加任何前缀,如 "/", "./" 或 "../".否则,Lip将拒绝安装这一tooth包。 - ## `possession` 声明哪些文件夹是由tooth包拥有的。卸载时,声明的文件夹中的文件将被删除。升级或重新安装时,在新旧两个版本的possession中都制定了的文件夹中的文件不会被移除(placement指定的除外)。 ### 语法 -列表中的每一项都应该是相对于BDS根的有效目录路径,以"/"结尾。 +列表中的每一项都应该是有效目录路径,以"/"结尾。 ### 样例 @@ -440,12 +434,10 @@ windows/arm64 ], "properties": { "source": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_]([a-zA-Z0-9-_\\.\/]*([a-zA-Z0-9-_]|\\/\\*))?$" + "type": "string" }, "destination": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_]([a-zA-Z0-9-_\\.\/]*([a-zA-Z0-9-_]|\\/\\*))?$" + "type": "string" }, "GOOS": { "type": "string" @@ -460,8 +452,7 @@ windows/arm64 "type": "array", "additionalItems": false, "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_][a-zA-Z0-9-_\\.\/]*\\/$" + "type": "string" } }, "commands": { diff --git a/src/cmd/install/install.go b/src/cmd/install/install.go index 8522b14..fca0fd5 100644 --- a/src/cmd/install/install.go +++ b/src/cmd/install/install.go @@ -19,6 +19,7 @@ type FlagDict struct { helpFlag bool upgradeFlag bool forceReinstallFlag bool + yesFlag bool } const helpMessage = ` @@ -35,7 +36,8 @@ Description: Options: -h, --help Show help. --upgrade Upgrade the specified tooth to the newest available version. - --force-reinstall Reinstall the tooth even if they are already up-to-date.` + --force-reinstall Reinstall the tooth even if they are already up-to-date. + -y, --yes Assume yes to all prompts and run non-interactively.` // Run is the entry point. func Run() { @@ -61,6 +63,9 @@ func Run() { flagSet.BoolVar(&flagDict.forceReinstallFlag, "force-reinstall", false, "") + flagSet.BoolVar(&flagDict.yesFlag, "yes", false, "") + flagSet.BoolVar(&flagDict.yesFlag, "y", false, "") + flagSet.Parse(os.Args[2:]) // Help flag has the highest priority. @@ -302,7 +307,7 @@ func Run() { // TODO: Check if the tooth file is manually installed. isManuallyInstalled := false - err = install(toothFile, isManuallyInstalled) + err = install(toothFile, isManuallyInstalled, flagDict.yesFlag) if err != nil { logger.Error(err.Error()) return diff --git a/src/cmd/install/utils.go b/src/cmd/install/utils.go index c7a0445..92392fe 100644 --- a/src/cmd/install/utils.go +++ b/src/cmd/install/utils.go @@ -19,6 +19,7 @@ import ( "github.com/liteldev/lip/tooth/toothfile" "github.com/liteldev/lip/tooth/toothrecord" "github.com/liteldev/lip/utils/download" + "github.com/liteldev/lip/utils/logger" versionutils "github.com/liteldev/lip/utils/version" ) @@ -163,7 +164,7 @@ func FetchVersionList(repoPath string) ([]versionutils.Version, error) { } // Install installs the .tth file. -func install(t toothfile.ToothFile, isManuallyInstalled bool) error { +func install(t toothfile.ToothFile, isManuallyInstalled bool, isYes bool) error { // 1. Check if the tooth is already installed. recordDir, err := localfile.RecordDir() @@ -179,37 +180,20 @@ func install(t toothfile.ToothFile, isManuallyInstalled bool) error { return errors.New("the tooth is already installed") } - // 2. Install the record file. + // 2. Place the files to the right place in the workspace. - // Create a record object from the metadata. - record := toothrecord.NewFromMetadata(t.Metadata(), isManuallyInstalled) - - // Encode the record object to JSON. - recordJSON, err := record.JSON() - if err != nil { - return err - } - - // Write the metadata bytes to the record file. - err = os.WriteFile(recordFilePath, recordJSON, 0755) + // Open the .tth file. + r, err := zip.OpenReader(t.FilePath()) if err != nil { - return errors.New("failed to write record file " + recordFilePath + " " + err.Error()) + return errors.New("failed to open tooth file " + t.FilePath()) } - - // 3. Place the files to the right place in the workspace. + defer r.Close() workSpaceDir, err := localfile.WorkSpaceDir() if err != nil { return err } - // Open the .tth file. - r, err := zip.OpenReader(t.FilePath()) - if err != nil { - return errors.New("failed to open tooth file " + t.FilePath()) - } - defer r.Close() - // Get the file prefix. filePrefix := toothfile.GetFilePrefix(r) @@ -223,7 +207,28 @@ func install(t toothfile.ToothFile, isManuallyInstalled bool) error { } source := placement.Source - destination := workSpaceDir + "/" + placement.Destination + destination := placement.Destination + + if !isYes { + workSpaceDirAbs, err := filepath.Abs(workSpaceDir) + if err != nil { + return errors.New("failed to get the absolute path of the workspace directory") + } + + destinationAbs, err := filepath.Abs(destination) + if err != nil { + return errors.New("failed to get the absolute path of the destination") + } + + relPath, err := filepath.Rel(workSpaceDirAbs, destinationAbs) + if err != nil || strings.HasPrefix(relPath, "../") || strings.HasPrefix(relPath, "..\\") { + ans := logger.Prompt("The destination " + destination + " is not in the workspace. Do you want to continue? (y/N)") + if ans != "y" && ans != "Y" { + return errors.New("installation aborted") + } + isYes = true + } + } // Create the parent directory of the destination. os.MkdirAll(filepath.Dir(destination), 0755) @@ -291,6 +296,23 @@ func install(t toothfile.ToothFile, isManuallyInstalled bool) error { } } + // 3. Install the record file. + + // Create a record object from the metadata. + record := toothrecord.NewFromMetadata(t.Metadata(), isManuallyInstalled) + + // Encode the record object to JSON. + recordJSON, err := record.JSON() + if err != nil { + return err + } + + // Write the metadata bytes to the record file. + err = os.WriteFile(recordFilePath, recordJSON, 0755) + if err != nil { + return errors.New("failed to write record file " + recordFilePath + " " + err.Error()) + } + return nil } diff --git a/src/cmd/uninstall/utils.go b/src/cmd/uninstall/utils.go index 7b0056f..1680f7f 100644 --- a/src/cmd/uninstall/utils.go +++ b/src/cmd/uninstall/utils.go @@ -9,6 +9,7 @@ import ( "github.com/liteldev/lip/localfile" "github.com/liteldev/lip/tooth/toothrecord" + "github.com/liteldev/lip/utils/logger" ) // Uninstall uninstalls a tooth. @@ -82,12 +83,7 @@ func Uninstall(recordFileName string, possessionList []string) error { continue } - workspaceDir, err := localfile.WorkSpaceDir() - if err != nil { - return err - } - - destination := workspaceDir + "/" + placement.Destination + destination := placement.Destination // Continue if the destination does not exist. if _, err := os.Stat(destination); os.IsNotExist(err) { @@ -96,7 +92,7 @@ func Uninstall(recordFileName string, possessionList []string) error { err = os.Remove(destination) if err != nil { - return errors.New("cannot delete the file " + destination + ": " + err.Error()) + logger.Error("cannot delete the file " + destination + ": " + err.Error() + ". Please delete it manually.") } // Delete the parent directory if it is empty. @@ -110,7 +106,7 @@ func Uninstall(recordFileName string, possessionList []string) error { if len(files) == 0 { err = os.Remove(parentDir) if err != nil { - return errors.New("cannot delete the directory " + parentDir + ": " + err.Error()) + logger.Error("cannot delete the directory " + parentDir + ": " + err.Error() + ". Please delete it manually.") } } } @@ -131,22 +127,17 @@ func Uninstall(recordFileName string, possessionList []string) error { continue } - workspaceDir, err := localfile.WorkSpaceDir() - if err != nil { - return err - } - // Remove the folder. - err = os.RemoveAll(workspaceDir + "/" + possession) + err = os.RemoveAll(possession) if err != nil { - return errors.New("cannot delete the folder " + workspaceDir + "/" + possession + ": " + err.Error()) + logger.Error("cannot delete the folder " + possession + ": " + err.Error() + ". Please delete it manually.") } } // Delete the record file. err = os.Remove(recordDir + "/" + recordFileName) if err != nil { - return errors.New("cannot delete the record file " + recordDir + "/" + recordFileName + ": " + err.Error()) + logger.Error("cannot delete the record file " + recordDir + "/" + recordFileName + ": " + err.Error() + ". Please delete it manually.") } return nil diff --git a/src/tooth/toothmetadata/metadata.go b/src/tooth/toothmetadata/metadata.go index dc5922c..098ae48 100644 --- a/src/tooth/toothmetadata/metadata.go +++ b/src/tooth/toothmetadata/metadata.go @@ -108,12 +108,10 @@ const jsonSchema string = ` ], "properties": { "source": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_]([a-zA-Z0-9-_\\.\/]*([a-zA-Z0-9-_]|\\/\\*))?$" + "type": "string" }, "destination": { - "type": "string", - "pattern": "^(.)?[a-zA-Z0-9-_]([a-zA-Z0-9-_\\.\/]*([a-zA-Z0-9-_]|\\/\\*))?$" + "type": "string" }, "GOOS": { "type": "string" @@ -128,8 +126,7 @@ const jsonSchema string = ` "type": "array", "additionalItems": false, "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_][a-zA-Z0-9-_\\.\/]*\\/$" + "type": "string" } }, "commands": { diff --git a/src/utils/logger/logger.go b/src/utils/logger/logger.go index 5483a8e..390779a 100644 --- a/src/utils/logger/logger.go +++ b/src/utils/logger/logger.go @@ -24,6 +24,13 @@ func Error(format string, a ...interface{}) { color.HiRed("ERROR: "+format, a...) } +func Prompt(format string, a ...interface{}) string { + var input string + fmt.Printf(format, a...) + fmt.Scanln(&input) + return input +} + // Fatal prints an error message to the console and exits. func SetColor(status bool) { color.NoColor = !status