From 5d24951ff99834d706e4c855a8fd945822b7fdfb Mon Sep 17 00:00:00 2001 From: lihan Date: Mon, 8 Apr 2024 11:26:22 +0800 Subject: [PATCH 1/5] add new hook `ParseLegacyFile` --- internal/interfaces.go | 10 ++++ internal/plugin.go | 54 +++++++++++++++++-- internal/plugin_test.go | 31 +++++++++++ .../testdata/plugins/java_with_main/main.lua | 20 +++++++ .../hooks/parse_legacy_file.lua | 17 ++++++ .../plugins/java_with_metadata/metadata.lua | 7 ++- 6 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 internal/testdata/plugins/java_with_metadata/hooks/parse_legacy_file.lua diff --git a/internal/interfaces.go b/internal/interfaces.go index 6cf3f77e..c6ad6933 100644 --- a/internal/interfaces.go +++ b/internal/interfaces.go @@ -154,6 +154,15 @@ type EnvKeysHookResultItem struct { Value string `luai:"value"` } +type ParseLegacyFileHookCtx struct { + Filepath string `luai:"filepath"` + Filename string `luai:"filename"` +} + +type ParseLegacyFileResult struct { + Version string `luai:"version"` +} + type LuaPluginInfo struct { Name string `luai:"name"` Version string `luai:"version"` @@ -164,6 +173,7 @@ type LuaPluginInfo struct { License string `luai:"license"` MinRuntimeVersion string `luai:"minRuntimeVersion"` Notes []string `luai:"notes"` + LegacyFilenames []string `luai:"legacyFilenames"` } // LuaRuntime represents the runtime information of the Lua environment. diff --git a/internal/plugin.go b/internal/plugin.go index 3cf5918d..45946c13 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -45,11 +45,12 @@ type HookFunc struct { var ( // HookFuncMap is a map of built-in hook functions. HookFuncMap = map[string]HookFunc{ - "Available": {Name: "Available", Required: true, Filename: "available"}, - "PreInstall": {Name: "PreInstall", Required: true, Filename: "pre_install"}, - "EnvKeys": {Name: "EnvKeys", Required: true, Filename: "env_keys"}, - "PostInstall": {Name: "PostInstall", Required: false, Filename: "post_install"}, - "PreUse": {Name: "PreUse", Required: false, Filename: "pre_use"}, + "Available": {Name: "Available", Required: true, Filename: "available"}, + "PreInstall": {Name: "PreInstall", Required: true, Filename: "pre_install"}, + "EnvKeys": {Name: "EnvKeys", Required: true, Filename: "env_keys"}, + "PostInstall": {Name: "PostInstall", Required: false, Filename: "post_install"}, + "PreUse": {Name: "PreUse", Required: false, Filename: "pre_use"}, + "ParseLegacyFile": {Name: "ParseLegacyFile", Required: false, Filename: "parse_legacy_file"}, } ) @@ -320,6 +321,49 @@ func (l *LuaPlugin) PreUse(version Version, previousVersion Version, scope UseSc return Version(result.Version), nil } +func (l *LuaPlugin) ParseLegacyFile(path string) (Version, error) { + if len(l.LegacyFilenames) == 0 { + return "", nil + } + if !l.HasFunction("ParseLegacyFile") { + return "", nil + } + + L := l.vm.Instance + + filename := filepath.Base(path) + + ctx := ParseLegacyFileHookCtx{ + Filepath: path, + Filename: filename, + } + + logger.Debugf("ParseLegacyFile: %+v", ctx) + + ctxTable, err := luai.Marshal(L, ctx) + if err != nil { + return "", err + } + + if err = l.CallFunction("ParseLegacyFile", ctxTable); err != nil { + return "", err + } + + table := l.vm.ReturnedValue() + if table == nil || table.Type() == lua.LTNil { + return "", nil + } + + result := &ParseLegacyFileResult{} + + if err := luai.Unmarshal(table, result); err != nil { + return "", err + } + + return Version(result.Version), nil + +} + func (l *LuaPlugin) CallFunction(funcName string, args ...lua.LValue) error { logger.Debugf("CallFunction: %s\n", funcName) if err := l.vm.CallFunction(l.pluginObj.RawGetString(funcName), append([]lua.LValue{l.pluginObj}, args...)...); err != nil { diff --git a/internal/plugin_test.go b/internal/plugin_test.go index 311a29d9..4d568430 100644 --- a/internal/plugin_test.go +++ b/internal/plugin_test.go @@ -2,6 +2,7 @@ package internal import ( "github.com/version-fox/vfox/internal/util" + "reflect" "strings" "testing" @@ -113,6 +114,10 @@ func TestNewLuaPluginWithMetadataAndHooks(t *testing.T) { if plugin.MinRuntimeVersion != "0.2.2" { t.Errorf("expected min runtime version '0.2.2', got '%s'", plugin.MinRuntimeVersion) } + if !reflect.DeepEqual(plugin.LegacyFilenames, []string{".node-version", ".nvmrc"}) { + t.Errorf("expected legacy filenames '.node-version', '.nvmrc', got '%s'", plugin.LegacyFilenames) + } + for _, hf := range HookFuncMap { if !plugin.HasFunction(hf.Name) && hf.Required { t.Errorf("expected to have function %s", hf.Name) @@ -288,4 +293,30 @@ func testHookFunc(t *testing.T, factory func() (*LuaPlugin, error)) { t.Errorf("expected version '1.0.0', got '%s'", version) } }) + + t.Run("ParseLegacyFile", func(t *testing.T) { + plugin, err := factory() + version, err := plugin.ParseLegacyFile("/path/to/legacy/.node-version") + if err != nil { + t.Fatal(err) + } + + if version != "14.17.0" { + t.Errorf("expected version '14.17.0', got '%s'", version) + } + version, err = plugin.ParseLegacyFile("/path/to/legacy/.nvmrc") + if err != nil { + t.Fatal(err) + } + + if version != "0.0.1" { + t.Errorf("expected version '0.0.1', got '%s'", version) + } + plugin.LegacyFilenames = []string{} + version, err = plugin.ParseLegacyFile("/path/to/legacy/.nvmrc") + if err != nil && version != "" { + t.Errorf("expected non version, got '%s'", version) + } + + }) } diff --git a/internal/testdata/plugins/java_with_main/main.lua b/internal/testdata/plugins/java_with_main/main.lua index 588cbdda..07b287f2 100644 --- a/internal/testdata/plugins/java_with_main/main.lua +++ b/internal/testdata/plugins/java_with_main/main.lua @@ -27,6 +27,10 @@ PLUGIN = { updateUrl = "{URL}/sdk.lua", -- minimum compatible vfox version minRuntimeVersion = "0.2.2", + legacyFilenames = { + ".node-version", + ".nvmrc" + } } --- Returns some pre-installed information, such as version number, download address, local files, etc. @@ -168,3 +172,19 @@ function PLUGIN:PreUse(ctx) version = "1.0.0" } end + +function PLUGIN:ParseLegacyFile(ctx) + printTable(ctx) + local filename = ctx.filename + local filepath = ctx.filepath + if filename == ".node-version" then + return { + version = "14.17.0" + } + else + return { + version = "0.0.1" + } + end + +end diff --git a/internal/testdata/plugins/java_with_metadata/hooks/parse_legacy_file.lua b/internal/testdata/plugins/java_with_metadata/hooks/parse_legacy_file.lua new file mode 100644 index 00000000..691f51d6 --- /dev/null +++ b/internal/testdata/plugins/java_with_metadata/hooks/parse_legacy_file.lua @@ -0,0 +1,17 @@ +--- Parse the legacy file found by vfox to determine the version of the tool. +--- Useful to extract version numbers from files like JavaScript's package.json or Golangs go.mod. +function PLUGIN:ParseLegacyFile(ctx) + printTable(ctx) + local filename = ctx.filename + local filepath = ctx.filepath + if filename == ".node-version" then + return { + version = "14.17.0" + } + else + return { + version = "0.0.1" + } + end + +end \ No newline at end of file diff --git a/internal/testdata/plugins/java_with_metadata/metadata.lua b/internal/testdata/plugins/java_with_metadata/metadata.lua index 7b0a4f44..c6370407 100644 --- a/internal/testdata/plugins/java_with_metadata/metadata.lua +++ b/internal/testdata/plugins/java_with_metadata/metadata.lua @@ -19,4 +19,9 @@ PLUGIN.description = "xxx" -- minimum compatible vfox version PLUGIN.minRuntimeVersion = "0.2.2" -PLUGIN.manifestUrl = "manifest.json" \ No newline at end of file +PLUGIN.manifestUrl = "manifest.json" + +PLUGIN.legacyFilenames = { + ".node-version", + ".nvmrc" +} \ No newline at end of file From 9bba69ce7df0d5517284faa8d330b902387d7b52 Mon Sep 17 00:00:00 2001 From: lihan Date: Mon, 8 Apr 2024 14:56:19 +0800 Subject: [PATCH 2/5] Manipulate legacy filenames file when adding, updating, or deleting --- cmd/commands/activate.go | 23 +++- cmd/commands/env.go | 213 +++++++++++++++++-------------- internal/manager.go | 92 ++++++++++++- internal/toolset/file_record.go | 63 +++++++++ internal/toolset/tool_version.go | 54 +------- 5 files changed, 294 insertions(+), 151 deletions(-) create mode 100644 internal/toolset/file_record.go diff --git a/cmd/commands/activate.go b/cmd/commands/activate.go index d676e82e..7e5c47a4 100644 --- a/cmd/commands/activate.go +++ b/cmd/commands/activate.go @@ -43,14 +43,27 @@ func activateCmd(ctx *cli.Context) error { } manager := internal.NewSdkManager() defer manager.Close() - tvs, err := toolset.NewMultiToolVersions([]string{ - manager.PathMeta.HomePath, - manager.PathMeta.WorkingDirectory, - }) + + workToolVersion, err := toolset.NewToolVersion(manager.PathMeta.WorkingDirectory) + if err != nil { + return err + } + + if err = manager.ParseLegacyFile(func(sdkname, version string) { + if _, ok := workToolVersion.Record[sdkname]; !ok { + workToolVersion.Record[sdkname] = version + } + }); err != nil { + return err + } + homeToolVersion, err := toolset.NewToolVersion(manager.PathMeta.HomePath) if err != nil { return err } - envKeys, err := manager.EnvKeys(tvs) + envKeys, err := manager.EnvKeys(toolset.MultiToolVersions{ + workToolVersion, + homeToolVersion, + }) if err != nil { return err } diff --git a/cmd/commands/env.go b/cmd/commands/env.go index 9d12f702..9bf71e7b 100644 --- a/cmd/commands/env.go +++ b/cmd/commands/env.go @@ -19,32 +19,30 @@ package commands import ( "encoding/json" "fmt" - "github.com/version-fox/vfox/internal/toolset" - "github.com/urfave/cli/v2" "github.com/version-fox/vfox/internal" "github.com/version-fox/vfox/internal/env" "github.com/version-fox/vfox/internal/shell" + "github.com/version-fox/vfox/internal/toolset" ) var Env = &cli.Command{ - Name: "env", - Hidden: true, + Name: "env", Flags: []cli.Flag{ &cli.StringFlag{ Name: "shell", Aliases: []string{"s"}, - Usage: "shell", + Usage: "shell name", }, &cli.BoolFlag{ Name: "cleanup", Aliases: []string{"c"}, - Usage: "cleanup temp file", + Usage: "cleanup old temp files", }, &cli.BoolFlag{ Name: "json", Aliases: []string{"j"}, - Usage: "get envs as json", + Usage: "output json format", }, }, Action: envCmd, @@ -52,101 +50,130 @@ var Env = &cli.Command{ func envCmd(ctx *cli.Context) error { if ctx.IsSet("json") { - type SDKs map[string]map[string]*string - data := struct { - IsHookEnv bool `json:"is_hook_env"` - Paths []string `json:"paths"` - SDKs SDKs `json:"sdks"` - }{ - IsHookEnv: env.IsHookEnv(), - Paths: []string{}, - SDKs: make(SDKs), - } - manager := internal.NewSdkManager() - defer manager.Close() - tvs, err := toolset.NewMultiToolVersions([]string{ - manager.PathMeta.WorkingDirectory, - manager.PathMeta.CurTmpPath, - manager.PathMeta.HomePath, - }) - if err != nil { - return err - } - tvs.FilterTools(func(name, version string) bool { - if lookupSdk, err := manager.LookupSdk(name); err == nil { - if keys, err := lookupSdk.EnvKeys(internal.Version(version)); err == nil { - data.SDKs[lookupSdk.Plugin.Name] = keys.Variables - data.Paths = append(data.Paths, keys.Paths.Slice()...) - return true - } - } - return false - }) - jsonData, err := json.Marshal(data) - if err != nil { - return err - } - fmt.Println(string(jsonData)) - return nil + return outputJSON() } else if ctx.IsSet("cleanup") { - manager := internal.NewSdkManager() - defer manager.Close() - // Clean up the old temp files, before today. - manager.CleanTmp() - return nil + return cleanTmp() } else { - shellName := ctx.String("shell") - if shellName == "" { - return cli.Exit("shell name is required", 1) - } - s := shell.NewShell(shellName) - if s == nil { - return fmt.Errorf("unknow target shell %s", shellName) - } - manager := internal.NewSdkManager() - defer manager.Close() + return envFlag(ctx) + } +} - tvs, err := toolset.NewMultiToolVersions([]string{ - manager.PathMeta.WorkingDirectory, - manager.PathMeta.CurTmpPath, - manager.PathMeta.HomePath, - }) - if err != nil { - return err +func outputJSON() error { + type SDKs map[string]map[string]*string + data := struct { + IsHookEnv bool `json:"is_hook_env"` + Paths []string `json:"paths"` + SDKs SDKs `json:"sdks"` + }{ + IsHookEnv: env.IsHookEnv(), + Paths: []string{}, + SDKs: make(SDKs), + } + manager := internal.NewSdkManager() + defer manager.Close() + tvs, err := toolset.NewMultiToolVersions([]string{ + manager.PathMeta.WorkingDirectory, + manager.PathMeta.CurTmpPath, + manager.PathMeta.HomePath, + }) + if err != nil { + return err + } + tvs.FilterTools(func(name, version string) bool { + if lookupSdk, err := manager.LookupSdk(name); err == nil { + if keys, err := lookupSdk.EnvKeys(internal.Version(version)); err == nil { + data.SDKs[lookupSdk.Plugin.Name] = keys.Variables + data.Paths = append(data.Paths, keys.Paths.Slice()...) + return true + } } + return false + }) + jsonData, err := json.Marshal(data) + if err != nil { + return err + } + fmt.Println(string(jsonData)) + return nil +} - envKeys, err := manager.EnvKeys(tvs) - if err != nil { - return err - } +func cleanTmp() error { + manager := internal.NewSdkManager() + defer manager.Close() + // Clean up the old temp files, before today. + manager.CleanTmp() + return nil +} - exportEnvs := make(env.Vars) - for k, v := range envKeys.Variables { - exportEnvs[k] = v - } - sdkPaths := envKeys.Paths +func envFlag(ctx *cli.Context) error { + shellName := ctx.String("shell") + if shellName == "" { + return cli.Exit("shell name is required", 1) + } + s := shell.NewShell(shellName) + if s == nil { + return fmt.Errorf("unknow target shell %s", shellName) + } + manager := internal.NewSdkManager() + defer manager.Close() - // Takes the complement of previousPaths and sdkPaths, and removes the complement from osPaths. - previousPaths := env.NewPaths(env.PreviousPaths) - for _, pp := range previousPaths.Slice() { - if sdkPaths.Contains(pp) { - previousPaths.Remove(pp) - } + envKeys, err := aggregateEnvKeys(manager) + if err != nil { + return err + } + + exportEnvs := make(env.Vars) + for k, v := range envKeys.Variables { + exportEnvs[k] = v + } + sdkPaths := envKeys.Paths + + // Takes the complement of previousPaths and sdkPaths, and removes the complement from osPaths. + previousPaths := env.NewPaths(env.PreviousPaths) + for _, pp := range previousPaths.Slice() { + if sdkPaths.Contains(pp) { + previousPaths.Remove(pp) } - osPaths := env.NewPaths(env.OsPaths) - if previousPaths.Len() != 0 { - for _, pp := range previousPaths.Slice() { - osPaths.Remove(pp) - } + } + osPaths := env.NewPaths(env.OsPaths) + if previousPaths.Len() != 0 { + for _, pp := range previousPaths.Slice() { + osPaths.Remove(pp) } - // Set the sdk paths to the new previous paths. - newPreviousPathStr := sdkPaths.String() - exportEnvs[env.PreviousPathsFlag] = &newPreviousPathStr + } + // Set the sdk paths to the new previous paths. + newPreviousPathStr := sdkPaths.String() + exportEnvs[env.PreviousPathsFlag] = &newPreviousPathStr - pathStr := sdkPaths.Merge(osPaths).String() - exportEnvs["PATH"] = &pathStr - exportStr := s.Export(exportEnvs) - fmt.Println(exportStr) - return nil + pathStr := sdkPaths.Merge(osPaths).String() + exportEnvs["PATH"] = &pathStr + exportStr := s.Export(exportEnvs) + fmt.Println(exportStr) + return nil +} + +func aggregateEnvKeys(manager *internal.Manager) (*env.Envs, error) { + workToolVersion, err := toolset.NewToolVersion(manager.PathMeta.WorkingDirectory) + if err != nil { + return nil, err } + + if err = manager.ParseLegacyFile(func(sdkname, version string) { + if _, ok := workToolVersion.Record[sdkname]; !ok { + workToolVersion.Record[sdkname] = version + } + }); err != nil { + return nil, err + } + tvs, err := toolset.NewMultiToolVersions([]string{ + manager.PathMeta.CurTmpPath, + manager.PathMeta.HomePath, + }) + if err != nil { + return nil, err + } + // Add the working directory to the first + tvs = append(toolset.MultiToolVersions{workToolVersion}, tvs...) + + return manager.EnvKeys(tvs) } diff --git a/internal/manager.go b/internal/manager.go index b292bcf4..9510851d 100644 --- a/internal/manager.go +++ b/internal/manager.go @@ -20,8 +20,12 @@ import ( "encoding/json" "errors" "fmt" + "github.com/pterm/pterm" + "github.com/version-fox/vfox/internal/config" + "github.com/version-fox/vfox/internal/env" "github.com/version-fox/vfox/internal/logger" "github.com/version-fox/vfox/internal/toolset" + "github.com/version-fox/vfox/internal/util" "io" "net" "net/http" @@ -30,11 +34,6 @@ import ( "path/filepath" "strconv" "strings" - - "github.com/pterm/pterm" - "github.com/version-fox/vfox/internal/config" - "github.com/version-fox/vfox/internal/env" - "github.com/version-fox/vfox/internal/util" ) const ( @@ -171,6 +170,19 @@ func (m *Manager) Remove(pluginName string) error { if err = os.RemoveAll(source.InstallPath); err != nil { return err } + // clear legacy filenames + if len(source.Plugin.LegacyFilenames) > 0 { + lfr, err := m.loadLegacyFileRecord() + if err != nil { + return err + } + for _, filename := range source.Plugin.LegacyFilenames { + delete(lfr.Record, filename) + } + if err = lfr.Save(); err != nil { + return fmt.Errorf("remove legacy filenames failed: %w", err) + } + } pterm.Printf("Remove %s plugin successfully! \n", pterm.LightGreen(pluginName)) return nil } @@ -242,6 +254,24 @@ func (m *Manager) Update(pluginName string) error { if err = os.Rename(tempPlugin.Path, sdk.Plugin.Path); err != nil { return fmt.Errorf("update %s plugin failed, err: %w", pluginName, err) } + + // update legacy filenames + if len(tempPlugin.LegacyFilenames) != len(sdk.Plugin.LegacyFilenames) { + logger.Debugf("Update legacy filenames for %s plugin, from: %+v to: %+v \n", pluginName, sdk.Plugin.LegacyFilenames, tempPlugin.LegacyFilenames) + lfr, err := m.loadLegacyFileRecord() + if err != nil { + return err + } + for _, filename := range sdk.Plugin.LegacyFilenames { + delete(lfr.Record, filename) + } + for _, filename := range tempPlugin.LegacyFilenames { + lfr.Record[filename] = pluginName + } + if err = lfr.Save(); err != nil { + return fmt.Errorf("update legacy filenames failed: %w", err) + } + } success = true // print some notes if there are if len(tempPlugin.Notes) != 0 { @@ -365,6 +395,21 @@ func (m *Manager) Add(pluginName, url, alias string) error { if err = os.Rename(tempPlugin.Path, installPath); err != nil { return fmt.Errorf("install plugin error: %w", err) } + + // set legacy filenames + if len(tempPlugin.LegacyFilenames) > 0 { + lfr, err := m.loadLegacyFileRecord() + if err != nil { + return err + } + for _, filename := range tempPlugin.LegacyFilenames { + lfr.Record[filename] = pname + } + if err = lfr.Save(); err != nil { + return fmt.Errorf("add legacy filenames failed: %w", err) + } + } + pterm.Println("Plugin info:") pterm.Println("Name ", "->", pterm.LightBlue(tempPlugin.Name)) pterm.Println("Version ", "->", pterm.LightBlue(tempPlugin.Version)) @@ -528,6 +573,43 @@ func (m *Manager) GetRegistryAddress(uri string) string { return pluginRegistryAddress + "/" + uri } +// loadLegacyFileRecord load legacy file record which store the mapping of legacy filename and sdk-name +func (m *Manager) loadLegacyFileRecord() (*toolset.FileRecord, error) { + file := filepath.Join(m.PathMeta.HomePath, ".legacy_filenames") + mapFile, err := toolset.NewFileRecord(file) + if err != nil { + return nil, fmt.Errorf("failed to read .legacy_filenames file %s: %w", file, err) + } + return mapFile, nil +} + +// ParseLegacyFile parse legacy file and output the sdkname and version +func (m *Manager) ParseLegacyFile(output func(sdkname, version string)) error { + legacyFileRecord, err := m.loadLegacyFileRecord() + if err != nil { + return err + } + + // There are some legacy files to be parsed. + if len(legacyFileRecord.Record) > 0 { + for filename, sdkname := range legacyFileRecord.Record { + path := filepath.Join(m.PathMeta.WorkingDirectory, filename) + if util.FileExists(path) { + if sdk, err := m.LookupSdk(sdkname); err == nil { + // The .tool-version in the current directory has the highest priority, + // checking to see if the version information in the legacy file exists in the former, + // and updating to the former record (Don’t fall into the file!) if it doesn't. + if version, err := sdk.Plugin.ParseLegacyFile(path); err == nil && version != "" { + output(sdkname, string(version)) + } + } + } + } + + } + return nil +} + func NewSdkManager() *Manager { meta, err := newPathMeta() if err != nil { diff --git a/internal/toolset/file_record.go b/internal/toolset/file_record.go new file mode 100644 index 00000000..9669840f --- /dev/null +++ b/internal/toolset/file_record.go @@ -0,0 +1,63 @@ +package toolset + +import ( + "bufio" + "fmt" + "github.com/version-fox/vfox/internal/util" + "os" + "strings" +) + +// FileRecord is a file that contains a map of string to string +type FileRecord struct { + Record map[string]string + Path string +} + +func (m *FileRecord) Save() error { + if len(m.Record) == 0 { + return nil + } + file, err := os.Create(m.Path) + if err != nil { + return fmt.Errorf("failed to create file record %s: %w", m.Path, err) + } + defer file.Close() + + for k, v := range m.Record { + _, err := fmt.Fprintf(file, "%s %s\n", k, v) + if err != nil { + return err + } + } + return nil +} + +// NewFileRecord creates a new FileRecord from a file +// if the file does not exist, an empty FileRecord is returned +func NewFileRecord(path string) (*FileRecord, error) { + versionsMap := make(map[string]string) + if util.FileExists(path) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, " ") + if len(parts) == 2 { + versionsMap[parts[0]] = parts[1] + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + } + return &FileRecord{ + Record: versionsMap, + Path: path, + }, nil +} diff --git a/internal/toolset/tool_version.go b/internal/toolset/tool_version.go index f03533e1..369e55e0 100644 --- a/internal/toolset/tool_version.go +++ b/internal/toolset/tool_version.go @@ -1,12 +1,8 @@ package toolset import ( - "bufio" "fmt" - "github.com/version-fox/vfox/internal/util" - "os" "path/filepath" - "strings" ) const filename = ".tool-versions" @@ -43,57 +39,19 @@ func (m MultiToolVersions) Save() error { return nil } +// ToolVersion represents a .tool-versions file type ToolVersion struct { - // Sdks sdkName -> version - Record map[string]string - path string -} - -func (t *ToolVersion) Save() error { - if len(t.Record) == 0 { - return nil - } - file, err := os.Create(t.path) - if err != nil { - return err - } - defer file.Close() - - for k, v := range t.Record { - _, err := fmt.Fprintf(file, "%s %s\n", k, v) - if err != nil { - return err - } - } - return nil + *FileRecord } func NewToolVersion(dirPath string) (*ToolVersion, error) { file := filepath.Join(dirPath, filename) - versionsMap := make(map[string]string) - if util.FileExists(file) { - file, err := os.Open(file) - if err != nil { - return nil, err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, " ") - if len(parts) == 2 { - versionsMap[parts[0]] = parts[1] - } - } - - if err := scanner.Err(); err != nil { - return nil, err - } + mapFile, err := NewFileRecord(file) + if err != nil { + return nil, fmt.Errorf("failed to read tool versions file %s: %w", file, err) } return &ToolVersion{ - Record: versionsMap, - path: file, + FileRecord: mapFile, }, nil } From 8aeb01876d429ee47b01c3719f0d54292aa13b18 Mon Sep 17 00:00:00 2001 From: lihan Date: Tue, 9 Apr 2024 14:31:54 +0800 Subject: [PATCH 3/5] feat(luai): support encode LGFunction. --- internal/luai/encode.go | 13 ++++++++++++- internal/luai/example_test.go | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/luai/encode.go b/internal/luai/encode.go index 1cff0f64..f6d5f28c 100644 --- a/internal/luai/encode.go +++ b/internal/luai/encode.go @@ -56,8 +56,12 @@ func Marshal(state *lua.LState, v any) (lua.LValue, error) { if err != nil { return nil, err } + if lf, ok := sub.(*lua.LFunction); ok { + state.SetField(table, tag, lf) + } else { + table.RawSetString(tag, sub) + } - table.RawSetString(tag, sub) } return table, nil case reflect.String: @@ -111,6 +115,13 @@ func Marshal(state *lua.LState, v any) (lua.LValue, error) { } return table, nil + case reflect.Func: + if reflected.Type().ConvertibleTo(reflect.TypeOf(lua.LGFunction(nil))) { + lf := reflected.Convert(reflect.TypeOf(lua.LGFunction(nil))).Interface().(lua.LGFunction) + return state.NewFunction(lf), nil + } else { + return nil, errors.New("marshal: unsupported function type " + reflected.Type().String()) + } default: return nil, errors.New("marshal: unsupported type " + reflected.Kind().String() + " for reflected ") } diff --git a/internal/luai/example_test.go b/internal/luai/example_test.go index 2ecb4b97..33d2d76d 100644 --- a/internal/luai/example_test.go +++ b/internal/luai/example_test.go @@ -316,3 +316,39 @@ func TestCases(t *testing.T) { }) } } + +func TestEncodeFunc(t *testing.T) { + + teardownSuite := setupSuite(t) + defer teardownSuite(t) + + t.Run("EncodeFunc", func(t *testing.T) { + testdata := struct { + Func1 func(*lua.LState) int + Func2 func(*lua.LState) int `luai:"f2"` + }{ + Func1: lua.LGFunction(func(L *lua.LState) int { + L.Push(lua.LString("hello, world")) + return 1 + }), + Func2: lua.LGFunction(func(L *lua.LState) int { + L.Push(lua.LString("good")) + return 1 + }), + } + L := NewLuaVM() + defer L.Close() + + table, err := Marshal(L.Instance, testdata) + if err != nil { + t.Fatalf("marshal map failed: %v", err) + } + L.Instance.SetGlobal("m", table) + if err := L.Instance.DoString(` + assert(m:Func1() == "hello, world") + assert(m:f2() == "good") + `); err != nil { + t.Errorf("map test failed: %v", err) + } + }) +} From cf9cb7aded584edd0fb9d4117dbc4caa4693e17f Mon Sep 17 00:00:00 2001 From: lihan Date: Tue, 9 Apr 2024 17:30:42 +0800 Subject: [PATCH 4/5] Support for ParseLegacyFile hook function to get installed version list --- internal/interfaces.go | 6 ++++-- internal/manager.go | 18 ++++++------------ internal/plugin.go | 17 ++++++++++++++--- internal/sdk.go | 17 ++++++++++++++--- internal/toolset/file_record.go | 12 +++++++----- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/internal/interfaces.go b/internal/interfaces.go index c6ad6933..d84dc1dd 100644 --- a/internal/interfaces.go +++ b/internal/interfaces.go @@ -18,6 +18,7 @@ package internal import ( "fmt" + lua "github.com/yuin/gopher-lua" ) type LuaCheckSum struct { @@ -155,8 +156,9 @@ type EnvKeysHookResultItem struct { } type ParseLegacyFileHookCtx struct { - Filepath string `luai:"filepath"` - Filename string `luai:"filename"` + Filepath string `luai:"filepath"` + Filename string `luai:"filename"` + GetInstalledVersions lua.LGFunction `luai:"getInstalledVersions"` } type ParseLegacyFileResult struct { diff --git a/internal/manager.go b/internal/manager.go index 9510851d..ceab01d4 100644 --- a/internal/manager.go +++ b/internal/manager.go @@ -99,11 +99,7 @@ func (m *Manager) LookupSdk(name string) (*Sdk, error) { return nil, fmt.Errorf("failed to migrate an old plug-in: %w", err) } } - luaPlugin, err := NewLuaPlugin(pluginPath, m) - if err != nil { - return nil, err - } - sdk, _ := NewSdk(m, luaPlugin) + sdk, _ := NewSdk(m, pluginPath) m.openSdks[strings.ToLower(name)] = sdk return sdk, nil } @@ -134,12 +130,7 @@ func (m *Manager) LoadAllSdk() (map[string]*Sdk, error) { } else { continue } - source, err := NewLuaPlugin(path, m) - if err != nil { - pterm.Printf("Failed to load %s plugin, err: %s\n", filepath.Dir(path), err) - continue - } - sdk, _ := NewSdk(m, source) + sdk, _ := NewSdk(m, path) sdkMap[strings.ToLower(sdkName)] = sdk m.openSdks[strings.ToLower(sdkName)] = sdk } @@ -576,6 +567,7 @@ func (m *Manager) GetRegistryAddress(uri string) string { // loadLegacyFileRecord load legacy file record which store the mapping of legacy filename and sdk-name func (m *Manager) loadLegacyFileRecord() (*toolset.FileRecord, error) { file := filepath.Join(m.PathMeta.HomePath, ".legacy_filenames") + logger.Debugf("Loading legacy file record %s \n", file) mapFile, err := toolset.NewFileRecord(file) if err != nil { return nil, fmt.Errorf("failed to read .legacy_filenames file %s: %w", file, err) @@ -595,11 +587,13 @@ func (m *Manager) ParseLegacyFile(output func(sdkname, version string)) error { for filename, sdkname := range legacyFileRecord.Record { path := filepath.Join(m.PathMeta.WorkingDirectory, filename) if util.FileExists(path) { + logger.Debugf("Parsing legacy file %s \n", path) if sdk, err := m.LookupSdk(sdkname); err == nil { // The .tool-version in the current directory has the highest priority, // checking to see if the version information in the legacy file exists in the former, // and updating to the former record (Don’t fall into the file!) if it doesn't. - if version, err := sdk.Plugin.ParseLegacyFile(path); err == nil && version != "" { + if version, err := sdk.ParseLegacyFile(path); err == nil && version != "" { + logger.Debugf("Found %s@%s in %s \n", sdkname, version, path) output(sdkname, string(version)) } } diff --git a/internal/plugin.go b/internal/plugin.go index 45946c13..674ab770 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -292,7 +292,7 @@ func (l *LuaPlugin) PreUse(version Version, previousVersion Version, scope UseSc ctx.InstalledSdks[string(lSdk.Version)] = lSdk } - logger.Debugf("PreUseHookCtx: %+v", ctx) + logger.Debugf("PreUseHookCtx: %+v \n", ctx) ctxTable, err := luai.Marshal(L, ctx) if err != nil { @@ -321,7 +321,7 @@ func (l *LuaPlugin) PreUse(version Version, previousVersion Version, scope UseSc return Version(result.Version), nil } -func (l *LuaPlugin) ParseLegacyFile(path string) (Version, error) { +func (l *LuaPlugin) ParseLegacyFile(path string, installedVersions func() []Version) (Version, error) { if len(l.LegacyFilenames) == 0 { return "", nil } @@ -336,9 +336,20 @@ func (l *LuaPlugin) ParseLegacyFile(path string) (Version, error) { ctx := ParseLegacyFileHookCtx{ Filepath: path, Filename: filename, + GetInstalledVersions: func(L *lua.LState) int { + versions := installedVersions() + logger.Debugf("Invoking GetInstalledVersions result: %+v \n", versions) + table, err := luai.Marshal(L, versions) + if err != nil { + L.RaiseError(err.Error()) + return 0 + } + L.Push(table) + return 1 + }, } - logger.Debugf("ParseLegacyFile: %+v", ctx) + logger.Debugf("ParseLegacyFile: %+v \n", ctx) ctxTable, err := luai.Marshal(L, ctx) if err != nil { diff --git a/internal/sdk.go b/internal/sdk.go index eb418e01..fde4f664 100644 --- a/internal/sdk.go +++ b/internal/sdk.go @@ -411,6 +411,12 @@ func (b *Sdk) Current() Version { return Version(current[b.Plugin.SdkName]) } +func (b *Sdk) ParseLegacyFile(path string) (Version, error) { + return b.Plugin.ParseLegacyFile(path, func() []Version { + return b.List() + }) +} + func (b *Sdk) Close() { b.Plugin.Close() } @@ -585,10 +591,15 @@ func (b *Sdk) label(version Version) string { return fmt.Sprintf("%s@%s", strings.ToLower(b.Plugin.Name), version) } -func NewSdk(manager *Manager, source *LuaPlugin) (*Sdk, error) { +// NewSdk creates a new SDK instance. +func NewSdk(manager *Manager, pluginPath string) (*Sdk, error) { + luaPlugin, err := NewLuaPlugin(pluginPath, manager) + if err != nil { + return nil, fmt.Errorf("failed to create lua plugin: %w", err) + } return &Sdk{ sdkManager: manager, - InstallPath: filepath.Join(manager.PathMeta.SdkCachePath, strings.ToLower(source.SdkName)), - Plugin: source, + InstallPath: filepath.Join(manager.PathMeta.SdkCachePath, strings.ToLower(luaPlugin.SdkName)), + Plugin: luaPlugin, }, nil } diff --git a/internal/toolset/file_record.go b/internal/toolset/file_record.go index 9669840f..30e2e12c 100644 --- a/internal/toolset/file_record.go +++ b/internal/toolset/file_record.go @@ -10,12 +10,13 @@ import ( // FileRecord is a file that contains a map of string to string type FileRecord struct { - Record map[string]string - Path string + Record map[string]string + Path string + isInitEmpty bool } func (m *FileRecord) Save() error { - if len(m.Record) == 0 { + if m.isInitEmpty && len(m.Record) == 0 { return nil } file, err := os.Create(m.Path) @@ -57,7 +58,8 @@ func NewFileRecord(path string) (*FileRecord, error) { } } return &FileRecord{ - Record: versionsMap, - Path: path, + Record: versionsMap, + Path: path, + isInitEmpty: len(versionsMap) == 0, }, nil } From 1bbc2135dde335870dff574a4e81b811b0c96073 Mon Sep 17 00:00:00 2001 From: lihan Date: Tue, 9 Apr 2024 17:41:33 +0800 Subject: [PATCH 5/5] fix test --- internal/plugin_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/plugin_test.go b/internal/plugin_test.go index 4d568430..02856734 100644 --- a/internal/plugin_test.go +++ b/internal/plugin_test.go @@ -296,7 +296,9 @@ func testHookFunc(t *testing.T, factory func() (*LuaPlugin, error)) { t.Run("ParseLegacyFile", func(t *testing.T) { plugin, err := factory() - version, err := plugin.ParseLegacyFile("/path/to/legacy/.node-version") + version, err := plugin.ParseLegacyFile("/path/to/legacy/.node-version", func() []Version { + return nil + }) if err != nil { t.Fatal(err) } @@ -304,7 +306,9 @@ func testHookFunc(t *testing.T, factory func() (*LuaPlugin, error)) { if version != "14.17.0" { t.Errorf("expected version '14.17.0', got '%s'", version) } - version, err = plugin.ParseLegacyFile("/path/to/legacy/.nvmrc") + version, err = plugin.ParseLegacyFile("/path/to/legacy/.nvmrc", func() []Version { + return nil + }) if err != nil { t.Fatal(err) } @@ -313,7 +317,9 @@ func testHookFunc(t *testing.T, factory func() (*LuaPlugin, error)) { t.Errorf("expected version '0.0.1', got '%s'", version) } plugin.LegacyFilenames = []string{} - version, err = plugin.ParseLegacyFile("/path/to/legacy/.nvmrc") + version, err = plugin.ParseLegacyFile("/path/to/legacy/.nvmrc", func() []Version { + return nil + }) if err != nil && version != "" { t.Errorf("expected non version, got '%s'", version) }