diff --git a/cmd/llpkgstore/internal/generate.go b/cmd/llpkgstore/internal/generate.go index da36d18..2a25cc2 100644 --- a/cmd/llpkgstore/internal/generate.go +++ b/cmd/llpkgstore/internal/generate.go @@ -9,6 +9,7 @@ import ( "github.com/goplus/llpkgstore/config" "github.com/goplus/llpkgstore/internal/actions/generator/llcppg" + "github.com/goplus/llpkgstore/internal/actions/llpkg" "github.com/goplus/llpkgstore/internal/file" "github.com/goplus/llpkgstore/internal/pc" "github.com/spf13/cobra" @@ -65,7 +66,7 @@ func runLLCppgGenerateWithDir(dir string) error { } } - generator := llcppg.New(dir, cfg.Upstream.Package.Name, tempDir) + generator := llcppg.New(dir, tempDir, llpkg.PackageName(filepath.Base(dir))) return generator.Generate(dir) } diff --git a/cmd/llpkgstore/internal/verification.go b/cmd/llpkgstore/internal/verification.go index 9179ec1..8ba428c 100644 --- a/cmd/llpkgstore/internal/verification.go +++ b/cmd/llpkgstore/internal/verification.go @@ -2,15 +2,14 @@ package internal import ( "encoding/json" - "fmt" "os" "os/exec" "path/filepath" - "github.com/goplus/llpkgstore/config" "github.com/goplus/llpkgstore/internal/actions" "github.com/goplus/llpkgstore/internal/actions/env" "github.com/goplus/llpkgstore/internal/actions/generator/llcppg" + "github.com/goplus/llpkgstore/internal/actions/llpkg" "github.com/spf13/cobra" ) @@ -24,11 +23,11 @@ var verificationCmd = &cobra.Command{ } func runLLCppgVerificationWithDir(dir string) error { - cfg, err := config.ParseLLPkgConfig(filepath.Join(dir, LLGOModuleIdentifyFile)) + pkg, err := llpkg.NewLLPkg(dir) if err != nil { - return fmt.Errorf("parse config error: %v", err) + return err } - uc, err := config.NewUpstreamFromConfig(cfg.Upstream) + uc, err := pkg.Upstream() if err != nil { return err } @@ -36,7 +35,7 @@ func runLLCppgVerificationWithDir(dir string) error { if err != nil { return err } - generator := llcppg.New(dir, cfg.Upstream.Package.Name, dir) + generator := llcppg.New(dir, dir, pkg.Name()) generated := filepath.Join(dir, ".generated") os.Mkdir(generated, 0777) diff --git a/internal/actions/actions.go b/internal/actions/actions.go index de8ffd7..1c8b2da 100644 --- a/internal/actions/actions.go +++ b/internal/actions/actions.go @@ -2,6 +2,7 @@ package actions import ( "encoding/json" + "errors" "fmt" "log" "os" @@ -13,8 +14,9 @@ import ( "strings" "sync" - "github.com/goplus/llpkgstore/config" "github.com/goplus/llpkgstore/internal/actions/env" + "github.com/goplus/llpkgstore/internal/actions/llpkg" + "github.com/goplus/llpkgstore/internal/actions/mappingtable" "github.com/goplus/llpkgstore/internal/actions/versions" "github.com/goplus/llpkgstore/internal/file" "github.com/goplus/llpkgstore/internal/pc" @@ -96,16 +98,24 @@ func shaFromTag(tag string) string { return strings.TrimSpace(string(ret)) } +func headSHA() (string, error) { + ret, err := exec.Command("git", "rev-list", "--max-parents", "0", "--abbrev-commit", "HEAD").CombinedOutput() + if err != nil { + return "", errors.New(string(ret)) + } + return strings.TrimSpace(string(ret)), nil +} + // parseMappedVersion splits the mapped version string into library name and version. // Input format: "clib/semver" where semver starts with 'v' // Panics if input format is invalid or version isn't valid semantic version -func parseMappedVersion(version string) (clib, mappedVersion string, err error) { +func parseMappedVersion(version string) (packageName llpkg.PackageName, mappedVersion string, err error) { arr := strings.Split(version, "/") if len(arr) != 2 { err = fmt.Errorf("actions: invalid mapped version format") return } - clib, mappedVersion = arr[0], arr[1] + packageName, mappedVersion = llpkg.PackageName(arr[0]), arr[1] if !semver.IsValid(mappedVersion) { err = fmt.Errorf("actions: invalid mapped version format: mappedVersion is not a semver") @@ -114,25 +124,35 @@ func parseMappedVersion(version string) (clib, mappedVersion string, err error) } // isValidLLPkg checks if directory contains both llpkg.cfg and llcppg.cfg -func isValidLLPkg(files []os.DirEntry) bool { +func isLLPkgRoot(path string) bool { + // don't retrieve files from pr changes, consider about maintenance case + files, err := os.ReadDir(path) + if err != nil { + return false + } fileMap := make(map[string]struct{}, len(files)) for _, file := range files { fileMap[filepath.Base(file.Name())] = struct{}{} } + _, hasLLPkg := fileMap["llpkg.cfg"] _, hasLLCppg := fileMap["llcppg.cfg"] - return hasLLCppg && hasLLPkg + _, hasGoMod := fileMap["go.mod"] + return hasLLCppg && hasLLPkg && hasGoMod } // checkLegacyVersion validates versioning strategy for legacy package submissions // Ensures semantic versioning compliance and proper branch maintenance strategy -func checkLegacyVersion(ver *versions.Versions, cfg config.LLPkgConfig, mappedVersion string, isLegacy bool) error { - if slices.Contains(ver.GoVersions(cfg.Upstream.Package.Name), mappedVersion) { +func checkLegacyVersion(ver *mappingtable.Versions, pkg *llpkg.LLPkg, mappedVersion string, isLegacy bool) error { + clibName := pkg.ClibName() + clibVersion := pkg.ClibVersion() + + if slices.Contains(ver.GoVersions(clibName), mappedVersion) { return fmt.Errorf("actions: repeat semver %s", mappedVersion) } - vers := ver.CVersions(cfg.Upstream.Package.Name) - currentVersion := versions.ToSemVer(cfg.Upstream.Package.Version) + vers := ver.CVersions(clibName) + currentVersion := clibVersion.ToSemVer() // skip when we're the only latest version or C version doesn't follow semver. if len(vers) == 0 || !semver.IsValid(currentVersion) { @@ -148,7 +168,7 @@ func checkLegacyVersion(ver *versions.Versions, cfg config.LLPkgConfig, mappedVe if isLatest { // case1: we're the latest version, but mapped version is not latest, invalid. // example: all version: 1.8.1 => v1.2.0 1.7.1 => v1.1.0 current: 1.9.1 => v1.0.0 - if semver.Compare(ver.LatestGoVersion(cfg.Upstream.Package.Name), mappedVersion) > 0 { + if semver.Compare(ver.LatestGoVersion(clibName), mappedVersion) > 0 { return fmt.Errorf("actions: mapped version should not less than the legacy one") } return nil @@ -186,11 +206,11 @@ func checkLegacyVersion(ver *versions.Versions, cfg config.LLPkgConfig, mappedVe // case5: we're the latest patch version for current major and minor, check the mapped version // our mapped version should be larger than the closest one. // example: current submit: 1.5.2 => v1.1.1, closest minor: 1.4.1 => v1.1.0, valid. - originalVersion := ver.SearchBySemVer(cfg.Upstream.Package.Name, vers[i]) + originalVersion := ver.SearchBySemVer(clibName, vers[i]) if originalVersion == "" { return fmt.Errorf("actions: cannot find original C version from semver") } - closestMappedVersion := ver.LatestGoVersionForCVersion(cfg.Upstream.Package.Name, originalVersion) + closestMappedVersion := ver.LatestGoVersionForCVersion(clibName, originalVersion) if closestMappedVersion == "" { return fmt.Errorf("cannot find latest Go version from C version") } @@ -201,6 +221,20 @@ func checkLegacyVersion(ver *versions.Versions, cfg config.LLPkgConfig, mappedVe return nil } +func readMappingTableCompatible() (*mappingtable.Versions, error) { + ver, created, err := mappingtable.FromRelease() + if err != nil { + return nil, err + } + // if specified release that stores `llpkgstore.json` has created, read llpkgstore.json from release. + // otherwise, read it from github pages for compatibility + if created { + return ver, nil + } + // fallback to legacy gh-pages + return mappingtable.Read("llpkgstore.json"), nil +} + func BuildBinaryZip(uc *upstream.Upstream) (zipFileName, zipFilePath string, err error) { tempDir, err := os.MkdirTemp("", "llpkg-tool") if err != nil { diff --git a/internal/actions/actions_test.go b/internal/actions/actions_test.go index 3cac235..512fbf0 100644 --- a/internal/actions/actions_test.go +++ b/internal/actions/actions_test.go @@ -10,16 +10,25 @@ import ( "github.com/goplus/llpkgstore/config" ) +func demoDir() (dir string, err error) { + dir, err = os.Getwd() + if err != nil { + return + } + // ../../_demo + + dir = filepath.Join("..", "..", "_demo") + return +} + func TestBuildBinaryZip(t *testing.T) { - dir, err := os.Getwd() + demoDir, err := demoDir() if err != nil { t.Error(err) return } // ../../_demo - demoDir := filepath.Join(filepath.Dir(filepath.Dir(dir)), "_demo") - cfg, err := config.ParseLLPkgConfig(filepath.Join(demoDir, "llpkg.cfg")) if err != nil { t.Error(err) diff --git a/internal/actions/api.go b/internal/actions/api.go index 84e891f..a431775 100644 --- a/internal/actions/api.go +++ b/internal/actions/api.go @@ -3,21 +3,18 @@ package actions import ( "context" - "errors" "fmt" "io" "mime" "net/http" - "os" - "path/filepath" "regexp" "strings" "time" "github.com/google/go-github/v69/github" - "github.com/goplus/llpkgstore/config" "github.com/goplus/llpkgstore/internal/actions/env" - "github.com/goplus/llpkgstore/internal/actions/versions" + "github.com/goplus/llpkgstore/internal/actions/llpkg" + "github.com/goplus/llpkgstore/internal/actions/mappingtable" "golang.org/x/sync/errgroup" ) @@ -30,18 +27,11 @@ const ( regexString = `Release-as:\s%s/v(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?` ) -// regex compiles a regular expression pattern to detect "Release-as" directives in commit messages -// Parameters: -// -// packageName: Name of the package to format into the regex pattern -// -// Returns: -// -// *regexp.Regexp: Compiled regular expression for version parsing -func regex(packageName string) *regexp.Regexp { +// compileRawCommitVersionRegex compiles a regular expression pattern to detect "Release-as" directives in commit messages +func compileRawCommitVersionRegex(packageNamePattern string) *regexp.Regexp { // format: Release-as: clib/semver(with v prefix) // Must have one space in the end of Release-as: - return regexp.MustCompile(fmt.Sprintf(regexString, packageName)) + return regexp.MustCompile(fmt.Sprintf(regexString, packageNamePattern)) } func binaryZip(packageName string) string { @@ -244,8 +234,8 @@ func (d *DefaultClient) removeLabel(labelName string) error { // Panics: // // If no valid version found in PR commits -func (d *DefaultClient) checkMappedVersion(packageName string) (mappedVersion string, err error) { - matchMappedVersion := regex(packageName) +func (d *DefaultClient) checkMappedVersion(pkg *llpkg.LLPkg) (mappedVersion string, err error) { + matchMappedVersion := compileRawCommitVersionRegex(pkg.Name().String()) allCommits, err := d.currentPRCommit() if err != nil { @@ -307,7 +297,7 @@ func (d *DefaultClient) mappedVersion() (string, error) { message := commit.GetCommit().GetMessage() // parse the mapped version - mappedVersion := regex(".*").FindString(message) + mappedVersion := compileRawCommitVersionRegex(".*").FindString(message) // mapped version not found, a normal commit? if mappedVersion == "" { return "", ErrNoMappedVersion @@ -450,7 +440,7 @@ func (d *DefaultClient) uploadArtifact(artifactID int64, release *github.Reposit fileName, ok := params["filename"] if !ok { - return errors.New("actions: no filename found in Content-Disposition") + return fmt.Errorf("actions: no filename found in Content-Disposition") } fmt.Printf("Upload %s to %s\n", fileName, release.GetName()) @@ -458,20 +448,24 @@ func (d *DefaultClient) uploadArtifact(artifactID int64, release *github.Reposit return d.uploadToRelease(fileName, resp.ContentLength, resp.Body, release) } -func (d *DefaultClient) uploadArtifactsToRelease(release *github.RepositoryRelease) (files []*os.File, err error) { +func (d *DefaultClient) uploadArtifactsToRelease(release *github.RepositoryRelease) error { ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) defer cancel() id, err := env.WorkflowRunID() + if err != nil { + return err + } + artifacts, _, err := d.client.Actions.ListWorkflowRunArtifacts(ctx, d.owner, d.repo, id, &github.ListOptions{}) if err != nil { - return nil, wrapActionError(err) + return wrapActionError(err) } if artifacts.GetTotalCount() == 0 { - return nil, errors.New("actions: no artifact found") + return fmt.Errorf("actions: no artifact found") } errGroup, _ := errgroup.WithContext(context.TODO()) @@ -485,7 +479,7 @@ func (d *DefaultClient) uploadArtifactsToRelease(release *github.RepositoryRelea }) } - return nil, errGroup.Wait() + return errGroup.Wait() } // removeBranch deletes a branch from the repository @@ -510,9 +504,9 @@ func (d *DefaultClient) removeBranch(branchName string) error { // // ver: Version store object // cfg: Package configuration -func (d *DefaultClient) checkVersion(ver *versions.Versions, cfg config.LLPkgConfig) error { +func (d *DefaultClient) checkVersion(ver *mappingtable.Versions, pkg *llpkg.LLPkg) error { // 4. Check MappedVersion - version, err := d.checkMappedVersion(cfg.Upstream.Package.Name) + version, err := d.checkMappedVersion(pkg) if err != nil { return err } @@ -526,268 +520,23 @@ func (d *DefaultClient) checkVersion(ver *versions.Versions, cfg config.LLPkgCon if err != nil { return err } - return checkLegacyVersion(ver, cfg, mappedVersion, isLegacy) -} - -// CheckPR validates PR changes and returns affected packages -// Returns: -// -// []string: List of affected package paths -func (d *DefaultClient) CheckPR() ([]string, error) { - // build a file path map - pathMap := map[string][]string{} - changedFilePaths, err := env.Changes() - if err != nil { - return nil, err - } - for _, path := range changedFilePaths { - dir := filepath.Dir(path) - // initialize the dir - pathMap[dir] = nil - } - - var allPaths []string - - ver := versions.Read("llpkgstore.json") - - for path := range pathMap { - // don't retrieve files from pr changes, consider about maintenance case - files, err := os.ReadDir(path) - if err != nil { - return nil, wrapActionError(err) - } - - if !isValidLLPkg(files) { - delete(pathMap, path) - continue - } - // 3. Check directory name - llpkgFile := filepath.Join(path, "llpkg.cfg") - cfg, err := config.ParseLLPkgConfig(llpkgFile) - if err != nil { - return nil, err - } - // in our design, directory name should equal to the package name, - // which means it's not required to be equal. - // - // However, at the current stage, if this is not equal, conan may panic, - // to aovid unexpected behavior, we assert it's equal temporarily. - // this logic may be changed in the future. - packageName := strings.TrimSpace(cfg.Upstream.Package.Name) - if packageName != path { - return nil, fmt.Errorf("actions: directory name is not equal to package name in llpkg.cfg") - } - err = d.checkVersion(ver, cfg) - if err != nil { - return nil, err - } - - allPaths = append(allPaths, path) - } - - // 1. Check there's only one directory in PR - if len(pathMap) > 1 { - return nil, fmt.Errorf("actions: too many to-be-converted directory") - } - - // 2. Check config files(llpkg.cfg and llcppg.cfg) - if len(pathMap) == 0 { - return nil, fmt.Errorf("actions: no valid config files, llpkg.cfg and llcppg.cfg must exist") - } - - return allPaths, nil -} - -// Postprocessing handles version tagging and record updates after PR merge -// Creates Git tags, updates version records, and cleans up legacy branches -func (d *DefaultClient) Postprocessing() error { - // https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push - sha, err := env.LatestCommitSHA() - if err != nil { - return err - } - // check it's associated with a pr - if !d.isAssociatedWithPullRequest(sha) { - // not a merge commit, skip it. - return fmt.Errorf("actions: not a merge request commit") - } - - version, err := d.mappedVersion() - if err != nil { - return err - } - - clib, mappedVersion, err := parseMappedVersion(version) - if err != nil { - return err - } - - // the pr has merged, so we can read it. - cfg, err := config.ParseLLPkgConfig(filepath.Join(clib, "llpkg.cfg")) - if err != nil { - return err - } - - // write it to llpkgstore.json - ver := versions.Read("llpkgstore.json") - ver.Write(clib, cfg.Upstream.Package.Version, mappedVersion) - - if hasTag(version) { - return fmt.Errorf("actions: tag has already existed") - } - - if err := d.createTag(version, sha); err != nil { - return err - } - - // create a release - release, err := d.createReleaseByTag(version) - if err != nil { - return err - } - - _, err = d.uploadArtifactsToRelease(release) - if err != nil { - return err - } - - // we have finished tagging the commit, safe to remove the branch - branchName, isLegacy, err := d.isLegacyVersion() - if err != nil { - return err - } - if isLegacy { - err = d.removeBranch(branchName) - } - return err - // move to website in Github Action... + return checkLegacyVersion(ver, pkg, mappedVersion, isLegacy) } -// Release must be called before Postprocessing -func (d *DefaultClient) Release() error { - version, err := d.mappedVersion() - if err != nil { - return err - } - - clibName, _, err := parseMappedVersion(version) +func (d *DefaultClient) commitMappingTable(ver *mappingtable.Versions) error { + sha, err := headSHA() if err != nil { return err } + // ignore error if created + d.createTag("_mappingtable", sha) - // the pr has merged, so we can read it. - cfg, err := config.ParseLLPkgConfig(filepath.Join(clibName, "llpkg.cfg")) + release, err := d.createReleaseByTag("_mappingtable") if err != nil { return err } - uc, err := config.NewUpstreamFromConfig(cfg.Upstream) - if err != nil { - return err - } - - zipFilename, zipFilePath, err := BuildBinaryZip(uc) - if err != nil { - return err - } - - // upload to artifacts in GitHub Action - // https://github.com/goplus/llpkg/pull/50/files#diff-95373be0ab51a56a2200c8c07981d82e81569f2cd1e4e2946e2002bb66de766fR56-R60 - return env.Setenv(env.Env{ - "BIN_PATH": zipFilePath, - "BIN_FILENAME": strings.TrimSuffix(zipFilename, ".zip"), - }) -} - -// CreateBranchFromLabel creates release branch based on label format -// Follows naming convention: release-branch./ -func (d *DefaultClient) CreateBranchFromLabel(labelName string) error { - // design: branch:release-branch.{CLibraryName}/{MappedVersion} - branchName := strings.TrimPrefix(strings.TrimSpace(labelName), LabelPrefix) - if branchName == labelName { - return fmt.Errorf("actions: invalid label name format") - } - - // fast-path: branch exists, can skip. - if d.hasBranch(branchName) { - return nil - } - version := strings.TrimPrefix(branchName, BranchPrefix) - if version == branchName { - return fmt.Errorf("actions: invalid label name format") - } - clib, _, err := parseMappedVersion(version) - if err != nil { - return err - } - // slow-path: check the condition if we can create a branch - // - // create a branch only when this version is legacy. - // according to branch maintenance strategy - - // get latest version of the clib - ver := versions.Read("llpkgstore.json") - - cversions := ver.CVersions(clib) - if len(cversions) == 0 { - return fmt.Errorf("actions: no clib found") - } - - if !versions.IsSemver(cversions) { - return fmt.Errorf("actions: c version dones't follow semver, skip maintaining") - } - - return d.createBranch(branchName, shaFromTag(version)) -} - -// CleanResource removes labels and resources after issue resolution -// Verifies issue closure via PR merge before deletion -func (d *DefaultClient) CleanResource() error { - issueEvent, err := IssueEvent() - if err != nil { - return err - } - - issueNumber := int(issueEvent["number"].(float64)) - regex := regexp.MustCompile(fmt.Sprintf(`(f|F)ix.*#%d`, issueNumber)) - - // 1. check this issue is closed by a PR - // In Github, close a issue with a commit whose message follows this format - // fix/Fix* #{IssueNumber} - found := false - allCommits, err := d.allCommits() - if err != nil { - return err - } - for _, commit := range allCommits { - message := commit.Commit.GetMessage() - - if regex.MatchString(message) && - d.isAssociatedWithPullRequest(commit.GetSHA()) { - found = true - break - } - } - - if !found { - return fmt.Errorf("actions: current issue isn't closed by merged PR") - } - - var labelName string - - // 2. find out the branch name from the label - for _, labels := range issueEvent["labels"].([]map[string]any) { - label := labels["name"].(string) - - if strings.HasPrefix(label, BranchPrefix) { - labelName = label - break - } - } - - if labelName == "" { - return fmt.Errorf("current issue hasn't labelled, this should not happen") - } + buf := strings.NewReader(ver.String()) - return d.removeLabel(labelName) + return d.uploadToRelease("llpkgstore.json", buf.Size(), buf, release) } diff --git a/internal/actions/api_test.go b/internal/actions/api_test.go index ce57523..f5e5dc5 100644 --- a/internal/actions/api_test.go +++ b/internal/actions/api_test.go @@ -3,11 +3,12 @@ package actions import ( "os" "os/exec" + "path/filepath" "strings" "testing" - "github.com/goplus/llpkgstore/config" - "github.com/goplus/llpkgstore/internal/actions/versions" + "github.com/goplus/llpkgstore/internal/actions/llpkg" + "github.com/goplus/llpkgstore/internal/actions/mappingtable" ) func TestHasTag(t *testing.T) { @@ -26,62 +27,96 @@ func TestHasTag(t *testing.T) { } } -func actionFn(branchName string, fn func(legacy bool) error) error { - return fn(strings.HasPrefix(branchName, BranchPrefix)) -} - -func TestLegacyVersion1(t *testing.T) { - testLLPkgConfig := `{ - "upstream": { - "package": { - "name": "cjson", - "version": "1.7.17" - } - } - }` - os.WriteFile(".llpkg.cfg", []byte(testLLPkgConfig), 0755) - defer os.Remove(".llpkg.cfg") - - b := []byte(`{ - "cjson": { - "versions" : { - "1.7.16": ["v0.1.0"], - "1.7.18": ["v0.1.2", "v0.1.3"], - "1.8.18": ["v0.1.0", "v0.1.1"] - } - } - }`) +func TestHeadSHA(t *testing.T) { + sha, err := headSHA() + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } - os.WriteFile(".llpkgstore.json", []byte(b), 0755) - defer os.Remove(".llpkgstore.json") + if sha != "6d38ce6" { + t.Errorf("unexpected sha: got %s want %s", sha, "6d38ce6") - cfg, _ := config.ParseLLPkgConfig(".llpkg.cfg") - ver := versions.Read(".llpkgstore.json") + } +} - err := actionFn("main", func(legacy bool) error { - return checkLegacyVersion(ver, cfg, "v0.1.1", legacy) - }) +func actionFn(branchName string, fn func(legacy bool) error) error { + return fn(strings.HasPrefix(branchName, BranchPrefix)) +} - if err == nil { - t.Errorf("unexpected behavior: %v", err) +func prepareEnv(llpkgConfig, mappingTable []byte) (testDir string, err error) { + testDir, err = os.MkdirTemp("", "action-test") + if err != nil { + return + } + err = os.WriteFile(filepath.Join(testDir, "llpkg.cfg"), []byte(llpkgConfig), 0755) + if err != nil { + os.RemoveAll(testDir) + return + } + err = os.WriteFile(filepath.Join(testDir, "llpkgstore.json"), mappingTable, 0644) + if err != nil { + os.RemoveAll(testDir) return } + os.WriteFile(filepath.Join(testDir, "go.mod"), []byte(`module cjson + go 1.22 + `), 0644) + + os.WriteFile(filepath.Join(testDir, "x.go"), []byte(`package cjson`), 0644) + return } -func TestLegacyVersion2(t *testing.T) { - testLLPkgConfig := `{ +func TestLegacyVersion(t *testing.T) { + testCases := []struct { + name string + branch string + llpkgContent string + mappedVersion string + mappingTableContent string + wantErr bool + }{ + { + name: "case1", + branch: "main", + mappedVersion: "v0.1.1", + wantErr: true, + llpkgContent: `{ + "upstream": { + "package": { + "name": "cjson", + "version": "1.7.17" + } + } + }`, + + mappingTableContent: `{ + "cjson": { + "versions" : { + "1.7.16": ["v0.1.0"], + "1.7.18": ["v0.1.2", "v0.1.3"], + "1.8.18": ["v0.1.0", "v0.1.1"] + } + } + }`, + }, + + { + name: "case2", + branch: "release-branch.cjson/v0.1.1", + mappedVersion: "v0.1.2", + wantErr: false, + llpkgContent: `{ "upstream": { "package": { "name": "cjson", "version": "1.7.19" } } - }` - os.WriteFile(".llpkg.cfg", []byte(testLLPkgConfig), 0755) - defer os.Remove(".llpkg.cfg") + }`, - b := []byte(`{ + mappingTableContent: `{ "cjson": { "versions" : { "1.8.18": ["v0.2.0", "v0.2.1"], @@ -89,38 +124,24 @@ func TestLegacyVersion2(t *testing.T) { "1.7.16: ["v1.1.0"] } } - }`) - - os.WriteFile(".llpkgstore.json", []byte(b), 0755) - defer os.Remove(".llpkgstore.json") - - cfg, _ := config.ParseLLPkgConfig(".llpkg.cfg") - ver := versions.Read(".llpkgstore.json") - - err := actionFn("release-branch.cjson/v0.1.1", func(legacy bool) error { - return checkLegacyVersion(ver, cfg, "v0.1.2", legacy) - }) - isValid := err == nil - - if !isValid { - t.Errorf("unexpected behavior: %v", err) - return - } -} - -func TestLegacyVersion3(t *testing.T) { - testLLPkgConfig := `{ + }`, + }, + + { + name: "case3", + branch: "main", + mappedVersion: "v0.3.0", + wantErr: false, + llpkgContent: `{ "upstream": { "package": { "name": "cjson", "version": "1.9.1" } } - }` - os.WriteFile(".llpkg.cfg", []byte(testLLPkgConfig), 0755) - defer os.Remove(".llpkg.cfg") + }`, - b := []byte(`{ + mappingTableContent: `{ "cjson": { "versions" : { "1.7.16": ["v0.1.0"], @@ -128,38 +149,24 @@ func TestLegacyVersion3(t *testing.T) { "1.8.18": ["v0.2.0", "v0.2.1"] } } - }`) - - os.WriteFile(".llpkgstore.json", []byte(b), 0755) - defer os.Remove(".llpkgstore.json") - - cfg, _ := config.ParseLLPkgConfig(".llpkg.cfg") - ver := versions.Read(".llpkgstore.json") - - err := actionFn("main", func(legacy bool) error { - return checkLegacyVersion(ver, cfg, "v0.3.0", legacy) - }) - isValid := err == nil - - if !isValid { - t.Errorf("unexpected behavior: %v", err) - return - } -} - -func TestLegacyVersion4(t *testing.T) { - testLLPkgConfig := `{ + }`, + }, + + { + name: "case4", + branch: "main", + mappedVersion: "v0.0.1", + wantErr: true, + llpkgContent: `{ "upstream": { "package": { "name": "cjson", "version": "1.9.1" } } - }` - os.WriteFile(".llpkg.cfg", []byte(testLLPkgConfig), 0755) - defer os.Remove(".llpkg.cfg") + }`, - b := []byte(`{ + mappingTableContent: `{ "cjson": { "versions" : { "1.8.18": ["v0.2.0", "v0.2.1"], @@ -167,37 +174,24 @@ func TestLegacyVersion4(t *testing.T) { "1.7.18": ["v0.1.1", "v0.1.2"] } } - }`) - - os.WriteFile(".llpkgstore.json", []byte(b), 0755) - defer os.Remove(".llpkgstore.json") - - cfg, _ := config.ParseLLPkgConfig(".llpkg.cfg") - ver := versions.Read(".llpkgstore.json") - - err := actionFn("main", func(legacy bool) error { - return checkLegacyVersion(ver, cfg, "v0.0.1", legacy) - }) - - if err == nil { - t.Errorf("unexpected behavior: %v", err) - return - } -} - -func TestLegacyVersion5(t *testing.T) { - testLLPkgConfig := `{ + }`, + }, + + { + name: "case5", + branch: "main", + mappedVersion: "v0.1.1", + wantErr: true, + llpkgContent: `{ "upstream": { "package": { "name": "cjson", "version": "1.7.19" } } - }` - os.WriteFile(".llpkg.cfg", []byte(testLLPkgConfig), 0755) - defer os.Remove(".llpkg.cfg") + }`, - b := []byte(`{ + mappingTableContent: `{ "cjson": { "versions" : { "1.7.16": ["v0.1.0"], @@ -205,20 +199,37 @@ func TestLegacyVersion5(t *testing.T) { "1.8.18": ["v0.2.0", "v0.2.1"] } } - }`) + }`, + }, + } - os.WriteFile(".llpkgstore.json", []byte(b), 0755) - defer os.Remove(".llpkgstore.json") + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + testDir, err := prepareEnv([]byte(tt.llpkgContent), []byte(tt.mappingTableContent)) + if err != nil { + t.Error(err) + return + } + defer os.RemoveAll(testDir) - cfg, _ := config.ParseLLPkgConfig(".llpkg.cfg") - ver := versions.Read(".llpkgstore.json") + pkg, err := llpkg.NewLLPkg(testDir) + if err != nil { + t.Error(err) + return + } + ver := mappingtable.Read(filepath.Join(testDir, "llpkgstore.json")) - err := actionFn("main", func(legacy bool) error { - return checkLegacyVersion(ver, cfg, "v0.1.1", legacy) - }) + err = actionFn(tt.branch, func(legacy bool) error { + return checkLegacyVersion(ver, pkg, tt.mappedVersion, legacy) + }) - if err == nil { - t.Errorf("unexpected behavior: %v", err) - return + if tt.wantErr && err == nil { + t.Error("unexpected no error") + } + if !tt.wantErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + }) } + } diff --git a/internal/actions/core.go b/internal/actions/core.go new file mode 100644 index 0000000..5dafc0e --- /dev/null +++ b/internal/actions/core.go @@ -0,0 +1,268 @@ +package actions + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/goplus/llpkgstore/internal/actions/env" + "github.com/goplus/llpkgstore/internal/actions/llpkg" + "github.com/goplus/llpkgstore/internal/actions/versions" +) + +// CheckPR validates PR changes and returns affected packages +// Returns: +// +// []string: List of affected package paths +func (d *DefaultClient) CheckPR() ([]string, error) { + // build a file path map + pathMap := map[string][]string{} + changedFilePaths, err := env.Changes() + if err != nil { + return nil, err + } + for _, path := range changedFilePaths { + dir := filepath.Dir(path) + // initialize the dir + pathMap[dir] = nil + } + + var allPaths []string + + ver, err := readMappingTableCompatible() + if err != nil { + return nil, err + } + + for path := range pathMap { + if !isLLPkgRoot(path) { + delete(pathMap, path) + continue + } + pkg, err := llpkg.NewLLPkg(path) + if err != nil { + return nil, err + } + err = d.checkVersion(ver, pkg) + if err != nil { + return nil, err + } + allPaths = append(allPaths, path) + } + + // 2. Check config files(llpkg.cfg and llcppg.cfg) + if len(pathMap) == 0 { + return nil, fmt.Errorf("actions: no valid config files, llpkg.cfg and llcppg.cfg must exist") + } + + return allPaths, nil +} + +// Postprocessing handles version tagging and record updates after PR merge +// Creates Git tags, updates version records, and cleans up legacy branches +func (d *DefaultClient) Postprocessing() error { + // https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push + sha, err := env.LatestCommitSHA() + if err != nil { + return err + } + // check it's associated with a pr + if !d.isAssociatedWithPullRequest(sha) { + // not a merge commit, skip it. + return fmt.Errorf("actions: not a merge request commit") + } + + version, err := d.mappedVersion() + if err != nil { + return err + } + + clib, mappedVersion, err := parseMappedVersion(version) + if err != nil { + return err + } + + // the pr has merged, so we can read it. + pkg, err := llpkg.FromPackageName(clib) + if err != nil { + return err + } + + if hasTag(version) { + return fmt.Errorf("actions: tag has already existed") + } + + if err := d.createTag(version, sha); err != nil { + return err + } + + // write it to llpkgstore.json + ver, err := readMappingTableCompatible() + if err != nil { + return err + } + ver.Write(pkg.ClibName(), pkg.ClibVersion(), mappedVersion) + + // commit changes + err = d.commitMappingTable(ver) + if err != nil { + return err + } + + // create a release + release, err := d.createReleaseByTag(version) + if err != nil { + return err + } + + err = d.uploadArtifactsToRelease(release) + if err != nil { + return err + } + + // we have finished tagging the commit, safe to remove the branch + branchName, isLegacy, err := d.isLegacyVersion() + if err != nil { + return err + } + if isLegacy { + err = d.removeBranch(branchName) + } + return err +} + +// Release must be called before Postprocessing +func (d *DefaultClient) Release() error { + version, err := d.mappedVersion() + if err != nil { + return err + } + + clibName, _, err := parseMappedVersion(version) + if err != nil { + return err + } + + // the pr has merged, so we can read it. + pkg, err := llpkg.FromPackageName(clibName) + if err != nil { + return err + } + uc, err := pkg.Upstream() + if err != nil { + return err + } + + zipFilename, zipFilePath, err := BuildBinaryZip(uc) + if err != nil { + return err + } + + // upload to artifacts in GitHub Action + // https://github.com/goplus/llpkg/pull/50/files#diff-95373be0ab51a56a2200c8c07981d82e81569f2cd1e4e2946e2002bb66de766fR56-R60 + return env.Setenv(env.Env{ + "BIN_PATH": zipFilePath, + "BIN_FILENAME": strings.TrimSuffix(zipFilename, ".zip"), + }) +} + +// CreateBranchFromLabel creates release branch based on label format +// Follows naming convention: release-branch./ +func (d *DefaultClient) CreateBranchFromLabel(labelName string) error { + // design: branch:release-branch.{CLibraryName}/{MappedVersion} + branchName := strings.TrimPrefix(strings.TrimSpace(labelName), LabelPrefix) + if branchName == labelName { + return fmt.Errorf("actions: invalid label name format") + } + + // fast-path: branch exists, can skip. + if d.hasBranch(branchName) { + return nil + } + version := strings.TrimPrefix(branchName, BranchPrefix) + if version == branchName { + return fmt.Errorf("actions: invalid label name format") + } + clibName, _, err := parseMappedVersion(version) + if err != nil { + return err + } + pkg, err := llpkg.FromPackageName(clibName) + if err != nil { + return err + } + // slow-path: check the condition if we can create a branch + // + // create a branch only when this version is legacy. + // according to branch maintenance strategy + + // get latest version of the clib + ver, err := readMappingTableCompatible() + if err != nil { + return err + } + + cversions := ver.CVersions(pkg.ClibName()) + if len(cversions) == 0 { + return fmt.Errorf("actions: no clib found") + } + + if !versions.IsSemver(cversions) { + return fmt.Errorf("actions: c version dones't follow semver, skip maintaining") + } + + return d.createBranch(branchName, shaFromTag(version)) +} + +// CleanResource removes labels and resources after issue resolution +// Verifies issue closure via PR merge before deletion +func (d *DefaultClient) CleanResource() error { + issueEvent, err := IssueEvent() + if err != nil { + return err + } + + issueNumber := int(issueEvent["number"].(float64)) + regex := regexp.MustCompile(fmt.Sprintf(`(f|F)ix.*#%d`, issueNumber)) + + // 1. check this issue is closed by a PR + // In Github, close a issue with a commit whose message follows this format + // fix/Fix* #{IssueNumber} + found := false + allCommits, err := d.allCommits() + if err != nil { + return err + } + for _, commit := range allCommits { + message := commit.Commit.GetMessage() + + if regex.MatchString(message) && + d.isAssociatedWithPullRequest(commit.GetSHA()) { + found = true + break + } + } + + if !found { + return fmt.Errorf("actions: current issue isn't closed by merged PR") + } + + var labelName string + + // 2. find out the branch name from the label + for _, labels := range issueEvent["labels"].([]map[string]any) { + label := labels["name"].(string) + + if strings.HasPrefix(label, BranchPrefix) { + labelName = label + break + } + } + + if labelName == "" { + return fmt.Errorf("current issue hasn't labelled, this should not happen") + } + + return d.removeLabel(labelName) +} diff --git a/internal/actions/env/env.go b/internal/actions/env/env.go index ee8b076..2e70d13 100644 --- a/internal/actions/env/env.go +++ b/internal/actions/env/env.go @@ -112,9 +112,6 @@ func WorkflowRunID() (id int64, err error) { return } id, err = strconv.ParseInt(runId, 10, 64) - if err != nil { - return - } return } diff --git a/internal/actions/generator/llcppg/llcppg.go b/internal/actions/generator/llcppg/llcppg.go index e219791..1f26d16 100644 --- a/internal/actions/generator/llcppg/llcppg.go +++ b/internal/actions/generator/llcppg/llcppg.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/goplus/llpkgstore/internal/actions/generator" + "github.com/goplus/llpkgstore/internal/actions/llpkg" "github.com/goplus/llpkgstore/internal/file" "github.com/goplus/llpkgstore/internal/hashutils" "github.com/goplus/llpkgstore/internal/pc" @@ -67,17 +68,17 @@ func isExitedUnexpectedly(err error) bool { type llcppgGenerator struct { dir string // llcppg.cfg abs path pcDir string - packageName string + packageName llpkg.PackageName } -func New(dir, packageName, pcDir string) generator.Generator { +func New(dir, pcDir string, packageName llpkg.PackageName) generator.Generator { return &llcppgGenerator{dir: dir, packageName: packageName, pcDir: pcDir} } // normalizeModulePath returns a normalized module path like // cjson => github.com/goplus/llpkg/cjson func (l *llcppgGenerator) normalizeModulePath() string { - return goplusRepo + l.packageName + return goplusRepo + l.packageName.String() } func (l *llcppgGenerator) findSymbJSON() string { @@ -134,7 +135,7 @@ func (l *llcppgGenerator) Generate(toDir string) error { return errors.Join(ErrLLCppgGenerate, err) } // check output again - generatedPath := filepath.Join(path, l.packageName) + generatedPath := filepath.Join(path, l.packageName.String()) if _, err := os.Stat(generatedPath); os.IsNotExist(err) { return errors.Join(ErrLLCppgCheck, errors.New("generate fail")) } diff --git a/internal/actions/generator/llcppg/llcppg_test.go b/internal/actions/generator/llcppg/llcppg_test.go index aceeaab..0dfa511 100644 --- a/internal/actions/generator/llcppg/llcppg_test.go +++ b/internal/actions/generator/llcppg/llcppg_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/goplus/llpkgstore/config" + "github.com/goplus/llpkgstore/internal/actions/llpkg" "github.com/goplus/llpkgstore/internal/hashutils" "golang.org/x/mod/modfile" ) @@ -125,7 +126,7 @@ func TestLLCppg(t *testing.T) { os.Mkdir("testgenerate", 0777) defer os.RemoveAll("testgenerate") path, _ := filepath.Abs("testgenerate") - generator := New(path, "cjson", path) + generator := New(path, path, llpkg.PackageName("cjson")) os.WriteFile("testgenerate/llcppg.cfg", []byte(testLLCppgConfig), 0755) os.WriteFile("testgenerate/llpkg.cfg", []byte(testLLPkgConfig), 0755) diff --git a/internal/actions/llpkg/package.go b/internal/actions/llpkg/package.go new file mode 100644 index 0000000..f53bdc5 --- /dev/null +++ b/internal/actions/llpkg/package.go @@ -0,0 +1,113 @@ +package llpkg + +import ( + "errors" + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + + "github.com/goplus/llpkgstore/config" + "github.com/goplus/llpkgstore/upstream" +) + +var ErrWrongPackagePath = errors.New("llpkg: wrong package path") + +func parsePackageName(goFile string) (string, error) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, goFile, nil, parser.PackageClauseOnly) + if err != nil { + return "", err + } + return file.Name.Name, nil +} + +// Wraps an error with the "llpkg" prefix for better context +func wrapLLPkgError(err error) error { + return fmt.Errorf("llpkg: %w", err) +} + +// LLPkg represents a language-linked package with configuration and metadata +type LLPkg struct { + cfg config.LLPkgConfig + + packageName string + packagePath string +} + +// Creates a new LLPkg instance by reading configuration from the specified path +func NewLLPkg(packagePath string) (*LLPkg, error) { + if packagePath == "" { + var err error + packagePath, err = os.Getwd() + if err != nil { + return nil, err + } + } + + l := &LLPkg{packagePath: packagePath} + + if err := l.readPackageConfig(); err != nil { + return nil, err + } + return l, nil +} + +func FromPackageName(pkgName PackageName) (*LLPkg, error) { + return NewLLPkg(pkgName.String()) +} + +// Returns the name of the package derived from the go.mod module path +func (p LLPkg) Name() PackageName { + return PackageName(p.packageName) +} + +// Returns the name from llpkg.cfg config +func (p LLPkg) ClibName() ClibName { + return ClibName(p.cfg.Upstream.Package.Name) +} + +// Returns the version from llpkg.cfg config +func (p LLPkg) ClibVersion() ClibVersion { + return ClibVersion(p.cfg.Upstream.Package.Version) +} + +// Retrieves the upstream source configuration for this package +func (p *LLPkg) Upstream() (*upstream.Upstream, error) { + return config.NewUpstreamFromConfig(p.cfg.Upstream) +} + +// Reads and validates the package configuration from llpkg.cfg and go.mod files +func (p *LLPkg) readPackageConfig() (err error) { + llpkgFileName := filepath.Join(p.packagePath, "llpkg.cfg") + cfg, err := config.ParseLLPkgConfig(llpkgFileName) + if err != nil { + err = wrapLLPkgError(err) + return + } + goFiles, _ := filepath.Glob(filepath.Join(p.packagePath, "*.go")) + + if len(goFiles) == 0 { + err = ErrWrongPackagePath + return + } + + var packageName string + + for _, goFile := range goFiles { + packageName, err = parsePackageName(goFile) + if packageName != "" { + break + } + } + + if packageName == "" { + err = ErrWrongPackagePath + return + } + + p.cfg = cfg + p.packageName = packageName + return +} diff --git a/internal/actions/llpkg/package_test.go b/internal/actions/llpkg/package_test.go new file mode 100644 index 0000000..a29b402 --- /dev/null +++ b/internal/actions/llpkg/package_test.go @@ -0,0 +1,106 @@ +package llpkg + +import ( + "os" + "path/filepath" + "testing" + + "github.com/goplus/llpkgstore/internal/file" +) + +func demoDir() (dir string, err error) { + dir, err = os.Getwd() + if err != nil { + return + } + // ../../../_demo + + dir = filepath.Join("..", "..", "..", "_demo") + return +} + +func checkName(t *testing.T, demoDir string, wantErr bool) { + pkg, err := NewLLPkg(demoDir) + if err != nil { + if !wantErr { + t.Error(err) + } + return + } + if wantErr { + t.Error("unexpected no error") + return + } + if pkg.Name() != "libcjson" { + t.Errorf("unexpected package name: want %s got %s", "libcjson", pkg.Name()) + } + + if pkg.ClibName() != "cjson" { + t.Errorf("unexpected llpkg package name: want %s got %s", "cjson", pkg.Name()) + } + + if pkg.ClibVersion() != "1.7.18" { + t.Errorf("unexpected llpkg package version: want %s got %s", "1.7.18", pkg.ClibVersion()) + } +} + +func TestReadConfig(t *testing.T) { + demoDir, err := demoDir() + if err != nil { + t.Error(err) + return + } + + t.Run("one-go-file", func(t *testing.T) { + tempGoFileName := filepath.Join(demoDir, "x.go") + err := os.WriteFile(tempGoFileName, []byte(`package libcjson`), 0644) + if err != nil { + t.Error(err) + return + } + defer file.RemovePattern(filepath.Join(demoDir, "*.go")) + + checkName(t, demoDir, false) + }) + + t.Run("multi-go-files", func(t *testing.T) { + tempGoFileName1 := filepath.Join(demoDir, "a.go") + err := os.WriteFile(tempGoFileName1, []byte(`package libcjson`), 0644) + if err != nil { + t.Error(err) + return + } + tempGoFileName2 := filepath.Join(demoDir, "x.go") + err = os.WriteFile(tempGoFileName2, []byte(`package cjson`), 0644) + if err != nil { + t.Error(err) + return + } + defer file.RemovePattern(filepath.Join(demoDir, "*.go")) + + checkName(t, demoDir, false) + }) + + t.Run("multi-go-files-fallback", func(t *testing.T) { + tempGoFileName1 := filepath.Join(demoDir, "a.go") + err := os.WriteFile(tempGoFileName1, []byte(``), 0644) + if err != nil { + t.Error(err) + return + } + tempGoFileName2 := filepath.Join(demoDir, "x.go") + err = os.WriteFile(tempGoFileName2, []byte(`package libcjson`), 0644) + if err != nil { + t.Error(err) + return + } + defer file.RemovePattern(filepath.Join(demoDir, "*.go")) + + checkName(t, demoDir, false) + }) + + t.Run("no-go-files", func(t *testing.T) { + checkName(t, demoDir, true) + }) + +} diff --git a/internal/actions/llpkg/type.go b/internal/actions/llpkg/type.go new file mode 100644 index 0000000..9910098 --- /dev/null +++ b/internal/actions/llpkg/type.go @@ -0,0 +1,25 @@ +package llpkg + +import "github.com/goplus/llpkgstore/internal/actions/versions" + +type ( + PackageName string + ClibName string + ClibVersion string +) + +func (p PackageName) String() string { + return string(p) +} + +func (p ClibName) String() string { + return string(p) +} + +func (p ClibVersion) String() string { + return string(p) +} + +func (p ClibVersion) ToSemVer() string { + return versions.ToSemVer(p.String()) +} diff --git a/internal/actions/versions/versions.go b/internal/actions/mappingtable/table.go similarity index 61% rename from internal/actions/versions/versions.go rename to internal/actions/mappingtable/table.go index 395046f..08633f2 100644 --- a/internal/actions/versions/versions.go +++ b/internal/actions/mappingtable/table.go @@ -1,16 +1,23 @@ -package versions +package mappingtable import ( + "context" "encoding/json" "io" "log" + "net/http" "os" "slices" + "time" + "github.com/goplus/llpkgstore/internal/actions/llpkg" + vrs "github.com/goplus/llpkgstore/internal/actions/versions" "github.com/goplus/llpkgstore/metadata" "golang.org/x/mod/semver" ) +const _releaseSource = "https://github.com/goplus/llpkg/releases/download/_mappingtable/llpkgstore.json" + // Versions is a mapping table implement for Github Action only. // It's recommend to use another implement in llgo for common usage. type Versions struct { @@ -50,6 +57,40 @@ func Read(fileName string) *Versions { panic(err) } + return readFromBytes(b, f.Name()) +} + +func FromRelease() (table *Versions, isCreated bool, err error) { + ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", _releaseSource, nil) + if err != nil { + return + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + // if specified release is not created yet, return a blank mapping table. + if resp.StatusCode == http.StatusNotFound { + table = readFromBytes(nil, "llpkgstore.json") + return + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + isCreated = true + table = readFromBytes(b, "llpkgstore.json") + return +} + +func readFromBytes(b []byte, fileName string) *Versions { m := metadata.MetadataMap{} if len(b) > 0 { @@ -58,14 +99,14 @@ func Read(fileName string) *Versions { return &Versions{ MetadataMap: m, - fileName: f.Name(), + fileName: fileName, } } // cVersions retrieves the version mappings for a specific C library. // It returns a map where keys are C library versions and values are supported Go versions. -func (v *Versions) cVersions(clib string) map[metadata.CVersion][]metadata.GoVersion { - versions := v.MetadataMap[clib] +func (v *Versions) cVersions(clib llpkg.ClibName) map[metadata.CVersion][]metadata.GoVersion { + versions := v.MetadataMap[clib.String()] if versions == nil { return nil } @@ -74,20 +115,20 @@ func (v *Versions) cVersions(clib string) map[metadata.CVersion][]metadata.GoVer // CVersions returns all available versions of the specified C library. // The versions are returned as semantic version strings. -func (v *Versions) CVersions(clib string) (ret []string) { - versions := v.MetadataMap[clib] +func (v *Versions) CVersions(clib llpkg.ClibName) (ret []string) { + versions := v.MetadataMap[clib.String()] if versions == nil { return } for version := range versions.Versions { - ret = append(ret, ToSemVer(version)) + ret = append(ret, vrs.ToSemVer(version)) } return } // GoVersions lists all Go versions associated with the given C library. -func (v *Versions) GoVersions(clib string) (ret []string) { - versions := v.MetadataMap[clib] +func (v *Versions) GoVersions(clib llpkg.ClibName) (ret []string) { + versions := v.MetadataMap[clib.String()] if versions == nil { return } @@ -98,12 +139,12 @@ func (v *Versions) GoVersions(clib string) (ret []string) { } // LatestGoVersionForCVersion finds the latest Go version compatible with a specific C library version. -func (v *Versions) LatestGoVersionForCVersion(clib, cver string) string { - version := v.MetadataMap[clib] +func (v *Versions) LatestGoVersionForCVersion(clib llpkg.ClibName, cver llpkg.ClibVersion) string { + version := v.MetadataMap[clib.String()] if version == nil { return "" } - goVersions := version.Versions[cver] + goVersions := version.Versions[cver.String()] if len(goVersions) == 0 { return "" } @@ -112,10 +153,10 @@ func (v *Versions) LatestGoVersionForCVersion(clib, cver string) string { } // SearchBySemVer looks up a C library version by its semantic version string. -func (v *Versions) SearchBySemVer(clib, semver string) string { +func (v *Versions) SearchBySemVer(clib llpkg.ClibName, semver string) llpkg.ClibVersion { for version := range v.cVersions(clib) { - if ToSemVer(version) == semver { - return version + if vrs.ToSemVer(version) == semver { + return llpkg.ClibVersion(version) } } return "" @@ -123,7 +164,7 @@ func (v *Versions) SearchBySemVer(clib, semver string) string { // LatestGoVersion retrieves the latest Go version associated with the specified C library. // It aggregates all Go versions across all C library versions and returns the highest one based on semantic versioning. -func (v *Versions) LatestGoVersion(clib string) string { +func (v *Versions) LatestGoVersion(clib llpkg.ClibName) string { versions := v.GoVersions(clib) if len(versions) == 0 { return "" @@ -140,19 +181,22 @@ func (v *Versions) LatestGoVersion(clib string) string { // mappedVersion: The Go version to map with the C library version. // // It appends the Go version to the existing list for the C library version and saves the updated metadata. -func (v *Versions) Write(clib, clibVersion, mappedVersion string) { - clibVersions := v.MetadataMap[clib] +func (v *Versions) Write(clib llpkg.ClibName, clibVersion llpkg.ClibVersion, mappedVersion string) { + clibName := clib.String() + cversion := clibVersion.String() + + clibVersions := v.MetadataMap[clibName] if clibVersions == nil { clibVersions = &metadata.Metadata{ Versions: map[metadata.CVersion][]metadata.GoVersion{}, } - v.MetadataMap[clib] = clibVersions + v.MetadataMap[clibName] = clibVersions } - versions := clibVersions.Versions[clibVersion] + versions := clibVersions.Versions[cversion] versions = appendVersion(versions, mappedVersion) - clibVersions.Versions[clibVersion] = versions + clibVersions.Versions[cversion] = versions // sync to disk b, _ := json.Marshal(&v.MetadataMap) diff --git a/internal/actions/versions/versions_test.go b/internal/actions/mappingtable/table_test.go similarity index 99% rename from internal/actions/versions/versions_test.go rename to internal/actions/mappingtable/table_test.go index 43f4ca8..b88d400 100644 --- a/internal/actions/versions/versions_test.go +++ b/internal/actions/mappingtable/table_test.go @@ -1,4 +1,4 @@ -package versions +package mappingtable import ( "bytes"