diff --git a/config/config.go b/config/config.go index 87a23fd4..9b61a082 100644 --- a/config/config.go +++ b/config/config.go @@ -170,6 +170,10 @@ func (c *Config) StateFile() string { return filepath.Join(c.CacheDir(), constants.StateFilename) } +func (c *Config) DepVersionFile() string { + return filepath.Join(c.CacheDir(), constants.DepVersionFilename) +} + func (c *Config) QuestionCacheFile(ext string) string { return filepath.Join(c.CacheDir(), constants.QuestionCacheBaseName+ext) } diff --git a/constants/constants.go b/constants/constants.go index 43056f2a..974a84b1 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -11,10 +11,8 @@ const ( ConfigFilename = "leetgo.yaml" QuestionCacheBaseName = "leetcode-questions" StateFilename = "state.json" + DepVersionFilename = "deps.json" CodeBeginMarker = "@lc code=begin" CodeEndMarker = "@lc code=end" ProjectURL = "https://github.com/j178/leetgo" - GoTestUtilsModPath = "github.com/j178/leetgo/testutils/go" - RustTestUtilsCrate = "leetgo_rs" - PythonTestUtilsMode = "leetgo_py" ) diff --git a/lang/base.go b/lang/base.go index 089c9ba8..c0df78f8 100644 --- a/lang/base.go +++ b/lang/base.go @@ -103,20 +103,14 @@ type Lang interface { ShortName() string // Slug returns the slug of the language. e.g. "cpp", "javascript", "python3" Slug() string + // InitWorkspace initializes the language workspace for code running. + InitWorkspace(dir string) error // Generate generates code files for the question. Generate(q *leetcode.QuestionData) (*GenerateResult, error) // GeneratePaths generates the paths of the code files for the question, without generating the real files. GeneratePaths(q *leetcode.QuestionData) (*GenerateResult, error) } -// NeedInitialization is an interface for languages that need to be initialized before generating code. -type NeedInitialization interface { - // HasInitialized returns whether the language workspace has been initialized. - HasInitialized(dir string) (bool, error) - // Initialize initializes the language workspace. - Initialize(dir string) error -} - // LocalTestable is an interface for languages that can run local test. type LocalTestable interface { // RunLocalTest runs local test for the question. @@ -369,6 +363,10 @@ func (l baseLang) ShortName() string { return l.shortName } +func (l baseLang) InitWorkspace(_ string) error { + return nil +} + func (l baseLang) generateCodeContent( q *leetcode.QuestionData, blocks []config.Block, diff --git a/lang/cpp.go b/lang/cpp.go index 1b3efb5d..31a312d0 100644 --- a/lang/cpp.go +++ b/lang/cpp.go @@ -8,7 +8,6 @@ import ( "github.com/google/shlex" "github.com/j178/leetgo/config" - "github.com/j178/leetgo/constants" "github.com/j178/leetgo/leetcode" cppUtils "github.com/j178/leetgo/testutils/cpp" "github.com/j178/leetgo/utils" @@ -18,7 +17,11 @@ type cpp struct { baseLang } -func (c cpp) Initialize(outDir string) error { +func (c cpp) InitWorkspace(outDir string) error { + if should, err := c.shouldInit(outDir); err != nil || !should { + return err + } + headerPath := filepath.Join(outDir, cppUtils.HeaderName) err := utils.WriteFile(headerPath, cppUtils.HeaderContent) if err != nil { @@ -29,28 +32,29 @@ func (c cpp) Initialize(outDir string) error { if err != nil { return err } - return nil + + err = UpdateDep(c) + return err } -func (c cpp) HasInitialized(outDir string) (bool, error) { +func (c cpp) shouldInit(outDir string) (bool, error) { headerPath := filepath.Join(outDir, cppUtils.HeaderName) if !utils.IsExist(headerPath) { - return false, nil + return true, nil } stdCxxPath := filepath.Join(outDir, "bits", "stdc++.h") if !utils.IsExist(stdCxxPath) { - return false, nil + return true, nil } - version, err := ReadVersion(headerPath) + update, err := IsDepUpdateToDate(c) if err != nil { return false, err } - currVersion := constants.Version - if version != currVersion { - return false, nil + if !update { + return true, nil } - return true, nil + return false, nil } var cppTypes = map[string]string{ diff --git a/lang/dep.go b/lang/dep.go new file mode 100644 index 00000000..60cc1613 --- /dev/null +++ b/lang/dep.go @@ -0,0 +1,83 @@ +package lang + +import ( + "errors" + "os" + + "github.com/goccy/go-json" + + "github.com/j178/leetgo/config" +) + +// If client dependency needs to be updated, update this version number. +var depVersions = map[string]int{ + cppGen.slug: 1, + golangGen.slug: 1, + python3Gen.slug: 1, + rustGen.slug: 1, +} + +func readDepVersions() (map[string]int, error) { + depVersionFile := config.Get().DepVersionFile() + records := make(map[string]int) + f, err := os.Open(depVersionFile) + if errors.Is(err, os.ErrNotExist) { + return records, nil + } + if err != nil { + return nil, err + } + defer f.Close() + err = json.NewDecoder(f).Decode(&records) + if err != nil { + return nil, err + } + return records, nil +} + +func IsDepUpdateToDate(lang Lang) (bool, error) { + ver := depVersions[lang.Slug()] + if ver == 0 { + return true, nil + } + + records, err := readDepVersions() + if err != nil { + return false, err + } + old := records[lang.Slug()] + if old == 0 || old != ver { + return false, nil + } + + return true, nil +} + +func UpdateDep(lang Lang) error { + ver := depVersions[lang.Slug()] + if ver == 0 { + return nil + } + + records, err := readDepVersions() + if err != nil { + return err + } + + records[lang.Slug()] = ver + + depVersionFile := config.Get().DepVersionFile() + f, err := os.Create(depVersionFile) + if err != nil { + return err + } + defer f.Close() + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + err = enc.Encode(records) + if err != nil { + return err + } + + return nil +} diff --git a/lang/gen.go b/lang/gen.go index 7ec12eb6..9fffd3bb 100644 --- a/lang/gen.go +++ b/lang/gen.go @@ -74,22 +74,9 @@ func generate(q *leetcode.QuestionData) (Lang, *GenerateResult, error) { return nil, nil, err } - // Check and generate necessary library files. - if t, ok := gen.(NeedInitialization); ok { - ok, err := t.HasInitialized(outDir) - if err == nil && !ok { - err = t.Initialize(outDir) - if err != nil { - return nil, nil, err - } - } - if err != nil { - log.Error( - "check initialization failed, skip initialization", - "lang", gen.Slug(), - "err", err, - ) - } + err = gen.InitWorkspace(outDir) + if err != nil { + return nil, nil, err } // Generate files diff --git a/lang/go.go b/lang/go.go index a64bea2f..e0546343 100644 --- a/lang/go.go +++ b/lang/go.go @@ -1,7 +1,6 @@ package lang import ( - "bytes" "fmt" "io" "os" @@ -12,11 +11,16 @@ import ( "github.com/charmbracelet/log" "github.com/j178/leetgo/config" - "github.com/j178/leetgo/constants" "github.com/j178/leetgo/leetcode" "github.com/j178/leetgo/utils" ) +const leetgoGo = "github.com/j178/leetgo/testutils/go" + +var goDeps = []string{ + leetgoGo + "@v0.2.0", +} + type golang struct { baseLang } @@ -102,26 +106,32 @@ func addMod(code string, q *leetcode.QuestionData) string { return strings.Join(newLines, "\n") } -func (g golang) HasInitialized(outDir string) (bool, error) { +func (g golang) shouldInit(outDir string) (bool, error) { if !utils.IsExist(filepath.Join(outDir, "go.mod")) { - return false, nil + return true, nil } - cmd := exec.Command("go", "list", "-m", "-json", constants.GoTestUtilsModPath) - cmd.Dir = outDir - output, err := cmd.CombinedOutput() + + update, err := IsDepUpdateToDate(g) if err != nil { - if bytes.Contains(output, []byte("not a known dependency")) || bytes.Contains( - output, - []byte("go.mod file not found"), - ) { - return false, nil - } - return false, fmt.Errorf("go list failed: %w: %s", err, output) + return false, err + } + if !update { + return true, nil } - return true, nil + return false, nil } -func (g golang) Initialize(outDir string) error { +func (g golang) InitWorkspace(outDir string) error { + if should, err := g.shouldInit(outDir); err != nil || !should { + return err + } + + err := utils.RemoveIfExist(filepath.Join(outDir, "go.mod")) + if err != nil { + return err + } + _ = utils.RemoveIfExist(filepath.Join(outDir, "go.sum")) + const modPath = "leetcode-solutions" var stderr strings.Builder cmd := exec.Command("go", "mod", "init", modPath) @@ -129,17 +139,23 @@ func (g golang) Initialize(outDir string) error { cmd.Dir = outDir cmd.Stdout = os.Stdout cmd.Stderr = io.MultiWriter(os.Stderr, &stderr) - err := cmd.Run() + err = cmd.Run() if err != nil && !strings.Contains(stderr.String(), "go.mod already exists") { return err } - cmd = exec.Command("go", "get", constants.GoTestUtilsModPath) + cmd = exec.Command("go", "get") + cmd.Args = append(cmd.Args, goDeps...) log.Info("go get", "cmd", cmd.String()) cmd.Dir = outDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() + if err != nil { + return err + } + + err = UpdateDep(g) return err } @@ -350,7 +366,7 @@ import ( "os" . "%s" -)`, constants.GoTestUtilsModPath, +)`, leetgoGo, ) testContent, err := g.generateTestContent(q) if err != nil { diff --git a/lang/python.go b/lang/python.go index b4e449fd..14d9f5d5 100644 --- a/lang/python.go +++ b/lang/python.go @@ -16,15 +16,23 @@ import ( "github.com/j178/leetgo/utils" ) -var requirements = fmt.Sprintf("sortedcontainers\n%s\n", constants.PythonTestUtilsMode) +const leetgoPy = "leetgo_py" + +var pyDeps = []string{ + "sortedcontainers==2.4.0", + leetgoPy + "==0.2.4", +} type python struct { baseLang } -func (p python) Initialize(outDir string) error { - pythonExe := config.Get().Code.Python.Executable +func (p python) InitWorkspace(outDir string) error { + if should, err := p.shouldInit(outDir); err != nil || !should { + return err + } + pythonExe := config.Get().Code.Python.Executable cmd := exec.Command(pythonExe, "--version") log.Info("checking python version", "cmd", cmd.String()) versionOutput, err := cmd.CombinedOutput() @@ -36,7 +44,12 @@ func (p python) Initialize(outDir string) error { return errors.New("python version must be 3.x") } - err = utils.WriteFile(path.Join(outDir, "requirements.txt"), []byte(requirements)) + err = utils.RemoveDirIfExist(path.Join(outDir, ".venv")) + if err != nil { + return err + } + + err = utils.WriteFile(path.Join(outDir, "requirements.txt"), []byte(strings.Join(pyDeps, "\n")+"\n")) if err != nil { return err } @@ -66,11 +79,23 @@ func (p python) Initialize(outDir string) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() + if err != nil { + return err + } + + err = UpdateDep(p) return err } -func (p python) HasInitialized(outDir string) (bool, error) { - return utils.IsExist(path.Join(outDir, ".venv")), nil +func (p python) shouldInit(outDir string) (bool, error) { + if !utils.IsExist(path.Join(outDir, ".venv")) { + return true, nil + } + update, err := IsDepUpdateToDate(p) + if err != nil { + return false, err + } + return !update, nil } func (p python) RunLocalTest(q *leetcode.QuestionData, outDir string, targetCase string) (bool, error) { @@ -270,7 +295,7 @@ func (p python) generateCodeFile( ) { codeHeader := fmt.Sprintf( `from typing import * -from %s import *`, constants.PythonTestUtilsMode, +from %s import *`, leetgoPy, ) testContent, err := p.generateTestContent(q) if err != nil { diff --git a/lang/rust.go b/lang/rust.go index ec545924..8dd94726 100644 --- a/lang/rust.go +++ b/lang/rust.go @@ -12,34 +12,57 @@ import ( "github.com/pelletier/go-toml/v2" "github.com/j178/leetgo/config" - "github.com/j178/leetgo/constants" "github.com/j178/leetgo/leetcode" "github.com/j178/leetgo/utils" ) +const leetgoRs = "leetgo-rs" + +var rustDeps = []string{ + "serde@1.0.196", + "serde_json@1.0.113", + "anyhow@1.0.79", + leetgoRs + "@0.2.1", +} + type rust struct { baseLang } -func (r rust) HasInitialized(outDir string) (bool, error) { +func (r rust) shouldInit(outDir string) (bool, error) { if !utils.IsExist(filepath.Join(outDir, "Cargo.toml")) { + return true, nil + } + update, err := IsDepUpdateToDate(r) + if err != nil { return false, nil } - return true, nil + return !update, nil } -func (r rust) Initialize(outDir string) error { +func (r rust) InitWorkspace(outDir string) error { + if should, err := r.shouldInit(outDir); err != nil || !should { + return err + } + + err := utils.RemoveIfExist(filepath.Join(outDir, "Cargo.toml")) + if err != nil { + return err + } + _ = utils.RemoveIfExist(filepath.Join(outDir, "Cargo.lock")) + const packageName = "leetcode-solutions" cmd := exec.Command("cargo", "init", "--bin", "--name", packageName, outDir) log.Info("cargo init", "cmd", cmd.String()) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = outDir - err := cmd.Run() + err = cmd.Run() if err != nil { return err } - cmd = exec.Command("cargo", "add", "serde", "serde_json", "anyhow", constants.RustTestUtilsCrate) + cmd = exec.Command("cargo", "add") + cmd.Args = append(cmd.Args, rustDeps...) log.Info("cargo add", "cmd", cmd.String()) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -48,7 +71,9 @@ func (r rust) Initialize(outDir string) error { if err != nil { return err } - return nil + + err = UpdateDep(r) + return err } func (r rust) RunLocalTest(q *leetcode.QuestionData, outDir string, targetCase string) (bool, error) { @@ -296,8 +321,7 @@ func (r rust) generateCodeFile( `use anyhow::Result; use %s::*; %s -`, constants.RustTestUtilsCrate, - emptySolution, +`, leetgoRs, emptySolution, ) testContent, err := r.generateTestContent(q) if err != nil { diff --git a/lang/version.go b/lang/version.go deleted file mode 100644 index e655d0d9..00000000 --- a/lang/version.go +++ /dev/null @@ -1,25 +0,0 @@ -package lang - -import ( - "bufio" - "os" - "strings" -) - -func ReadVersion(file string) (string, error) { - headerFile, err := os.Open(file) - if err != nil { - return "", err - } - defer headerFile.Close() - - scanner := bufio.NewScanner(headerFile) - if scanner.Scan() { - versionLine := scanner.Text() - version := versionLine[strings.Index(versionLine, "version: ")+len("version: "):] - version = strings.TrimSpace(version) - return version, nil - } - - return "", scanner.Err() -} diff --git a/testutils/cpp/header.go b/testutils/cpp/header.go index ba856da9..35606667 100644 --- a/testutils/cpp/header.go +++ b/testutils/cpp/header.go @@ -2,9 +2,6 @@ package cpp import ( _ "embed" - "fmt" - - "github.com/j178/leetgo/constants" ) const HeaderName = "LC_IO.h" @@ -18,7 +15,3 @@ var StdCxxContent []byte //go:embed LC_IO.h var HeaderContent []byte - -func init() { - HeaderContent = fmt.Appendf(nil, "// version: %s\n%s", constants.Version, HeaderContent) -} diff --git a/utils/file.go b/utils/file.go index bd182ad5..2db51ec1 100644 --- a/utils/file.go +++ b/utils/file.go @@ -62,6 +62,14 @@ func RemoveIfExist(path string) error { return err } +func RemoveDirIfExist(path string) error { + err := os.RemoveAll(path) + if os.IsNotExist(err) { + return nil + } + return err +} + func Truncate(filename string) error { f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC, 0o755) if err != nil {