forked from misaka00251/go2spec
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4f9ae0066 | ||
|
|
3d651215da | ||
|
|
9bc310f72a | ||
|
|
57fb207289 | ||
|
|
b04996dd02 | ||
|
|
14ad496479 | ||
|
|
50dca06727 | ||
|
|
6d6647bbdf |
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, Institute of Software, Chinese Academy of Sciences (ISCAS)
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES, LOSS OF USE, DATA, OR PROFITS, OR BUSINESS INTERRUPTION HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT INCLUDING NEGLIGENCE OR OTHERWISE ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,75 @@
|
||||
# go2spec
|
||||
|
||||
[](#overview)
|
||||
[](LICENSE)
|
||||
[](README.md)
|
||||
[](README.zh.md)
|
||||
|
||||
## Overview
|
||||
|
||||
go2spec is a Go package helper for the openRuyi RPM packaging workflow. It collects upstream metadata from the Go ecosystem, generates source archives, and writes RPM spec files.
|
||||
|
||||
## Installation
|
||||
|
||||
### Build From Source
|
||||
|
||||
```bash
|
||||
git clone <go2spec-repo-url>
|
||||
cd go2spec
|
||||
go build -o go2spec .
|
||||
```
|
||||
|
||||
Run it with:
|
||||
|
||||
```bash
|
||||
./go2spec help
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Generate a spec
|
||||
|
||||
```bash
|
||||
go2spec pack golang.org/x/net
|
||||
```
|
||||
|
||||
The command creates a file layout similar to this in the current directory:
|
||||
|
||||
```text
|
||||
go-golang-x-net/
|
||||
go-golang-x-net/go-golang-x-net.spec
|
||||
go-golang-x-net_<version>.orig.tar.<gz|xz>
|
||||
```
|
||||
|
||||
### Check existing spec updates without writing files
|
||||
|
||||
```bash
|
||||
go2spec update -n /path/to/SPECS
|
||||
```
|
||||
|
||||
`update` scans `/path/to/SPECS/go-*/*.spec`, then checks the latest upstream version.
|
||||
|
||||
### Run updates concurrently
|
||||
|
||||
```bash
|
||||
go2spec update -j 4 /path/to/SPECS
|
||||
```
|
||||
|
||||
> Always run dry run mode first before modifying real spec files.
|
||||
|
||||
## Documentation
|
||||
|
||||
| Area | English | Chinese |
|
||||
|---|---|---|
|
||||
| Main README | [README.md](README.md) | [README.zh.md](docs/zh/README.zh.md) |
|
||||
| `pack` command | [docs/en/pack.en.md](docs/en/pack.en.md) | [docs/zh/pack.zh.md](docs/zh/pack.zh.md) |
|
||||
| `update` command | [docs/en/update.en.md](docs/en/update.en.md) | [docs/zh/update.zh.md](docs/zh/update.zh.md) |
|
||||
|
||||
## Contributing
|
||||
|
||||
- Read the [openRuyi Code of Conduct](https://openruyi.cn/governance/legal/code-of-conduct) and agree to follow it.
|
||||
- Read the [AI-assisted Contribution Policy](https://openruyi.cn/governance/policy/ai-contribution-policy), and note that this Pull Request contains substantial AI-assisted content when applicable.
|
||||
|
||||
## License
|
||||
|
||||
go2spec uses the BSD-3-Clause license. See [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,75 @@
|
||||
# go2spec
|
||||
|
||||
[](#项目概览)
|
||||
[](LICENSE)
|
||||
[](README.md)
|
||||
[](README.zh.md)
|
||||
|
||||
## 项目概览
|
||||
|
||||
go2spec 是面向 openRuyi RPM 打包流程的 Go 包辅助工具。它从 Go 生态获取上游元数据,生成源码归档,并输出 RPM spec。
|
||||
|
||||
## 安装指南
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
git clone <go2spec-repo-url>
|
||||
cd go2spec
|
||||
go build -o go2spec .
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
./go2spec help
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 生成 spec
|
||||
|
||||
```bash
|
||||
go2spec pack golang.org/x/net
|
||||
```
|
||||
|
||||
该命令会在当前目录生成类似下面的文件结构:
|
||||
|
||||
```text
|
||||
go-golang-x-net/
|
||||
go-golang-x-net/go-golang-x-net.spec
|
||||
go-golang-x-net_<version>.orig.tar.<gz|xz>
|
||||
```
|
||||
|
||||
### 不写入文件地检查现有 spec 更新
|
||||
|
||||
```bash
|
||||
go2spec update -n /path/to/SPECS
|
||||
```
|
||||
|
||||
`update` 会扫描 `/path/to/SPECS/go-*/*.spec`,然后检查上游最新版本。
|
||||
|
||||
### 并发执行更新
|
||||
|
||||
```bash
|
||||
go2spec update -j 4 /path/to/SPECS
|
||||
```
|
||||
|
||||
> 建议始终先执行 dry run,再修改真实 spec 文件。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 内容 | English | 中文 |
|
||||
|---|---|---|
|
||||
| 主 README | [README.md](README.md) | [README.zh.md](README.zh.md) |
|
||||
| `pack` 命令 | [docs/en/pack.en.md](docs/en/pack.en.md) | [docs/zh/pack.zh.md](docs/zh/pack.zh.md) |
|
||||
| `update` 命令 | [docs/en/update.en.md](docs/en/update.en.md) | [docs/zh/update.zh.md](docs/zh/update.zh.md) |
|
||||
|
||||
## 贡献
|
||||
|
||||
- 阅读 [openRuyi 行为准则](https://openruyi.cn/governance/legal/code-of-conduct) 并同意遵守。
|
||||
- 阅读 [AI 辅助贡献政策](https://openruyi.cn/governance/policy/ai-contribution-policy),且此 Pull Request 包含实质性的 AI 辅助内容时需要说明。
|
||||
|
||||
## 许可证
|
||||
|
||||
go2spec 使用 BSD-3-Clause 许可证,见 [LICENSE](LICENSE)。
|
||||
+34
-4
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/mod/module"
|
||||
"log"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
@@ -36,6 +37,18 @@ func shortCommitHash(hash string) string {
|
||||
return hash
|
||||
}
|
||||
|
||||
func preferredRevisionCandidates(preferredRev string) []string {
|
||||
preferredRev = strings.TrimSpace(preferredRev)
|
||||
if preferredRev == "" {
|
||||
return nil
|
||||
}
|
||||
candidates := []string{preferredRev}
|
||||
if !strings.HasPrefix(preferredRev, "v") && semverRegexp.MatchString("v"+preferredRev) {
|
||||
candidates = append(candidates, "v"+preferredRev)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
// pkgVersionFromGit determines the actual version to be packaged
|
||||
// from the git repository status and user preference.
|
||||
// Besides returning the upstream version, the "upstream" struct fields
|
||||
@@ -47,13 +60,30 @@ func pkgVersionFromGit(gitdir string, u *upstream, preferredRev string, forcePre
|
||||
var commitsAhead int
|
||||
|
||||
var cmd *exec.Cmd // the temporary shell commands we execute
|
||||
preferredRev = strings.TrimSpace(preferredRev)
|
||||
|
||||
if module.IsPseudoVersion(preferredRev) {
|
||||
commit, err := module.PseudoVersionRev(preferredRev)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse pseudo-version %q: %w", preferredRev, err)
|
||||
}
|
||||
cmd = exec.Command("git", "rev-parse", "--verify", commit+"^{commit}")
|
||||
cmd.Dir = gitdir
|
||||
if _, err := cmd.Output(); err != nil {
|
||||
return "", fmt.Errorf("git revision %q: %w", commit, err)
|
||||
}
|
||||
u.commitIsh = commit
|
||||
u.version = strings.TrimPrefix(preferredRev, "v")
|
||||
return u.version, nil
|
||||
}
|
||||
|
||||
// If the user specifies a valid tag as the preferred revision, that tag should be used without additional heuristics.
|
||||
if preferredRev != "" {
|
||||
cmd = exec.Command("git", "tag", "--list", preferredRev)
|
||||
for _, candidate := range preferredRevisionCandidates(preferredRev) {
|
||||
cmd = exec.Command("git", "tag", "--list", candidate)
|
||||
cmd.Dir = gitdir
|
||||
if out, err := cmd.Output(); err == nil && slices.Contains(strings.Fields(string(out)), preferredRev) {
|
||||
latestTag = preferredRev
|
||||
if out, err := cmd.Output(); err == nil && slices.Contains(strings.Fields(string(out)), candidate) {
|
||||
latestTag = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# pack Command
|
||||
|
||||
[](../zh/pack.zh.md)
|
||||
[](update.en.md)
|
||||
|
||||
`pack` generates an RPM spec draft and a source archive from a Go import path.
|
||||
|
||||
**This command is intended to produce a maintainable starting point, not a final package that can be submitted directly. Always review the generated spec manually before submitting it to openRuyi or using it for builds.**
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
go2spec pack [FLAG]... <go-package-importpath>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
go2spec pack golang.org/x/net
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
```text
|
||||
-git_revision <rev>
|
||||
Check out a specific git revision, such as a tag, branch, or commit
|
||||
|
||||
-force_prerelease
|
||||
Package HEAD or tip instead of preferring the latest tagged version
|
||||
|
||||
-type <type>
|
||||
Select the package type, supported values are library, program, library+program, and program+library
|
||||
|
||||
-program_package_name <name>
|
||||
Override the program package name, and the source package name when appropriate
|
||||
|
||||
-allow_unknown_hoster
|
||||
Allow package-name generation for unknown hosters, Review the generated name manually
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
A successful run creates these files in the current directory:
|
||||
|
||||
- `<package-name>/`, package working directory
|
||||
- `<package-name>/<package-name>.spec`, generated RPM spec draft
|
||||
- `<package-name>_<version>.orig.tar.<gz|xz>`, upstream source archive
|
||||
|
||||
If the target package directory already exists and is not empty, go2spec aborts to avoid overwriting existing work.
|
||||
|
||||
## Package Types
|
||||
|
||||
- `library`, for Go library modules
|
||||
- `program`, for command packages whose root package is `main`
|
||||
- `library+program`, for projects that ship both library files and command subpackages
|
||||
- `program+library`, for program-first projects that also need a library subpackage
|
||||
|
||||
When `-type` is omitted, go2spec uses pkg.go.dev package metadata and local `go list` output to infer the type.
|
||||
@@ -0,0 +1,46 @@
|
||||
# update Command
|
||||
|
||||
[](../zh/update.zh.md)
|
||||
[](pack.en.md)
|
||||
|
||||
`update` scans existing Go spec packages, checks the latest upstream version, and can update the spec version.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
go2spec update [options] <SPECS_DIR>
|
||||
```
|
||||
|
||||
`<SPECS_DIR>` must contain `go-*` subdirectories with spec files.
|
||||
|
||||
```text
|
||||
SPECS_DIR/
|
||||
go-golang-x-net/
|
||||
go-golang-x-net/go-golang-x-net.spec
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```text
|
||||
-n
|
||||
Dry run, detect updates only, do not modify files
|
||||
|
||||
-j <num>
|
||||
Number of concurrent checks, default is 3
|
||||
```
|
||||
|
||||
## Recommended Workflow
|
||||
|
||||
Start with dry run mode:
|
||||
|
||||
```bash
|
||||
go2spec update -n /path/to/SPECS
|
||||
```
|
||||
|
||||
Apply updates after reviewing the output:
|
||||
|
||||
```bash
|
||||
go2spec update -j 4 /path/to/SPECS
|
||||
```
|
||||
|
||||
Review the generated diff before committing packaging changes.
|
||||
@@ -0,0 +1,58 @@
|
||||
# pack 命令
|
||||
|
||||
[](../en/pack.en.md)
|
||||
[](update.zh.md)
|
||||
|
||||
`pack` 根据 Go import path 生成 RPM spec 初稿和源码归档。
|
||||
|
||||
**该命令用于生成可继续维护的起点,不是最终可直接提交的包,提交到 openRuyi 或用于构建前必须人工 review 生成的 spec。**
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
go2spec pack [FLAG]... <go-package-importpath>
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
go2spec pack golang.org/x/net
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
```text
|
||||
-git_revision <rev>
|
||||
checkout 指定 git revision,例如 tag、branch 或 commit
|
||||
|
||||
-force_prerelease
|
||||
打包 HEAD 或 tip,而不是优先选择最新 tag
|
||||
|
||||
-type <type>
|
||||
选择包类型,支持 library、program、library+program 和 program+library
|
||||
|
||||
-program_package_name <name>
|
||||
覆盖程序包名,适用时也覆盖源包名
|
||||
|
||||
-allow_unknown_hoster
|
||||
允许未知 hoster 参与包名生成,生成结果必须人工检查
|
||||
```
|
||||
|
||||
## 输出
|
||||
|
||||
执行成功后会在当前目录创建:
|
||||
|
||||
- `<package-name>/`,package 工作目录
|
||||
- `<package-name>/<package-name>.spec`,生成的 RPM spec 初稿
|
||||
- `<package-name>_<version>.orig.tar.<gz|xz>`,上游源码归档
|
||||
|
||||
如果目标 package 目录已存在且非空,go2spec 会停止执行,以避免覆盖已有工作。
|
||||
|
||||
## 包类型
|
||||
|
||||
- `library`,适合 Go library module
|
||||
- `program`,适合根 package 为 `main` 的命令包
|
||||
- `library+program`,适合同时发布库文件和命令子包的项目
|
||||
- `program+library`,适合以程序为主,同时需要库子包的项目
|
||||
|
||||
不传 `-type` 时,go2spec 会根据 pkg.go.dev package 元数据和本地 `go list` 输出推断类型。
|
||||
@@ -0,0 +1,46 @@
|
||||
# update 命令
|
||||
|
||||
[](../en/update.en.md)
|
||||
[](pack.zh.md)
|
||||
|
||||
`update` 扫描已有 Golang spec 包,检查上游最新版本,并可更新 spec 版本。
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
go2spec update [options] <SPECS_DIR>
|
||||
```
|
||||
|
||||
`<SPECS_DIR>` 必须包含带 spec 文件的 `go-*` 子目录。
|
||||
|
||||
```text
|
||||
SPECS_DIR/
|
||||
go-golang-x-net/
|
||||
go-golang-x-net/go-golang-x-net.spec
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
```text
|
||||
-n
|
||||
dry run,只检测更新,不修改文件
|
||||
|
||||
-j <num>
|
||||
并发检查数量,默认 3
|
||||
```
|
||||
|
||||
## 推荐流程
|
||||
|
||||
先执行 dry run:
|
||||
|
||||
```bash
|
||||
go2spec update -n /path/to/SPECS
|
||||
```
|
||||
|
||||
确认输出后再执行更新:
|
||||
|
||||
```bash
|
||||
go2spec update -j 4 /path/to/SPECS
|
||||
```
|
||||
|
||||
提交 packaging 变更前必须 review 生成的 diff。
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"golang.org/x/mod/modfile"
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"io"
|
||||
"log"
|
||||
@@ -56,6 +58,12 @@ type upstream struct {
|
||||
|
||||
var errUnsupportedHoster = errors.New("unsupported hoster")
|
||||
|
||||
type packRequest struct {
|
||||
arg string
|
||||
gopkg string
|
||||
revision string
|
||||
}
|
||||
|
||||
func passthroughEnv() []string {
|
||||
var relevantVariables = []string{
|
||||
"HOME",
|
||||
@@ -178,13 +186,20 @@ func (u *upstream) get(gopath, sourceRepo, requestedPath, rev string) error {
|
||||
}
|
||||
|
||||
if rev != "" {
|
||||
cmd = exec.Command("git", "-c", "advice.detachedHead=false", "checkout", rev)
|
||||
checkoutRev := rev
|
||||
if module.IsPseudoVersion(rev) {
|
||||
checkoutRev, err = module.PseudoVersionRev(rev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse pseudo-version %q: %w", rev, err)
|
||||
}
|
||||
}
|
||||
cmd = exec.Command("git", "-c", "advice.detachedHead=false", "checkout", checkoutRev)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = passthroughEnv()
|
||||
cmd.Stderr = os.Stderr
|
||||
log.Println("get: Running", cmd, "in", cmd.Dir)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("git checkout %q: %w", rev, err)
|
||||
return fmt.Errorf("git checkout %q: %w", checkoutRev, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -960,10 +975,12 @@ func mainPack(args []string, usage func()) {
|
||||
flagSet.Usage = usage
|
||||
} else {
|
||||
flagSet.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [pack] [FLAG]... <go-package-importpath>\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Example: %s pack golang.org/x/oauth2\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [pack] [FLAG]... <go-package-importpath>[@revision]...\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s [pack] [FLAG]... -list <go.mod>\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Example: %s pack golang.org/x/oauth2@v0.30.0\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Example: %s pack -list packages.go.mod\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, "\"%s pack\" downloads the specified Go package from the Internet,\nand creates new files and directories in the current working directory.\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "\"%s pack\" downloads the specified Go packages from the Internet,\nand creates new files and directories in the current working directory.\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, "Flags:\n")
|
||||
flagSet.PrintDefaults()
|
||||
@@ -974,10 +991,16 @@ func mainPack(args []string, usage func()) {
|
||||
flagSet.StringVar(&gitRevision,
|
||||
"git_revision",
|
||||
"",
|
||||
"git revision (see gitrevisions(7)) of the specified Go package\n"+
|
||||
"git revision (see gitrevisions(7)) for package arguments without @revision\n"+
|
||||
"to check out, defaulting to the default behavior of git clone.\n"+
|
||||
"Useful in case you do not want to package e.g. current HEAD.")
|
||||
|
||||
var listFile string
|
||||
flagSet.StringVar(&listFile,
|
||||
"list",
|
||||
"",
|
||||
"Read packages to pack from the require directives of a go.mod file")
|
||||
|
||||
var forcePrerelease bool
|
||||
flagSet.BoolVar(&forcePrerelease,
|
||||
"force_prerelease",
|
||||
@@ -1020,35 +1043,13 @@ func mainPack(args []string, usage func()) {
|
||||
log.Fatalf("parse args: %v", err)
|
||||
}
|
||||
|
||||
// Check for required positional argument
|
||||
if flagSet.NArg() < 1 {
|
||||
// A package argument or a package-list file is required.
|
||||
if flagSet.NArg() < 1 && listFile == "" {
|
||||
flagSet.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
gitRevision = strings.TrimSpace(gitRevision)
|
||||
gopkg := flagSet.Arg(0)
|
||||
|
||||
// Remove URL scheme if present (https://, http://, git://, etc.)
|
||||
gopkg = strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(gopkg, "https://"), "http://"), "git://")
|
||||
|
||||
// Verify the provided argument using the official pkg.go.dev API. Keep the
|
||||
// requested path for naming and go_import_path; use the module path only for
|
||||
// source checkout and tarball generation.
|
||||
info, err := getPkgsiteInfo(context.TODO(), gopkg)
|
||||
if err != nil {
|
||||
log.Fatalf("Verifying arguments: %v — did you specify a Go package import path?", err)
|
||||
}
|
||||
sourceGopkg := sourceImportPathForPackage(gopkg, info)
|
||||
if sourceGopkg != gopkg {
|
||||
log.Printf("Using module root %q as source checkout for specified import path %q", sourceGopkg, gopkg)
|
||||
}
|
||||
|
||||
// Set default source and binary package names.
|
||||
openRuyiSrc := nameFromGopkg(gopkg, typeLibrary, customProgPkgName, allowUnknownHoster)
|
||||
openRuyiLib := openRuyiSrc
|
||||
openRuyiProgram := nameFromGopkg(gopkg, typeProgram, customProgPkgName, allowUnknownHoster)
|
||||
|
||||
var pkgType packageType
|
||||
|
||||
switch strings.TrimSpace(pkgTypeString) {
|
||||
@@ -1069,6 +1070,172 @@ func mainPack(args []string, usage func()) {
|
||||
log.Fatalf("-type=%q not recognized, aborting\n", pkgTypeString)
|
||||
}
|
||||
|
||||
// Now we collect all package's info into packRequest structs
|
||||
requests, err := collectPackRequests(flagSet.Args(), listFile, gitRevision)
|
||||
if err != nil {
|
||||
log.Fatalf("parse package list: %v", err)
|
||||
}
|
||||
|
||||
// Now we verify that all packages are valid
|
||||
if err := preflightPackRequests(context.TODO(), requests, pkgType, customProgPkgName, allowUnknownHoster); err != nil {
|
||||
log.Fatalf("preflight package list: %v", err)
|
||||
}
|
||||
|
||||
for _, req := range requests {
|
||||
packPackage(req.gopkg, req.revision, forcePrerelease, pkgType, customProgPkgName, allowUnknownHoster)
|
||||
}
|
||||
}
|
||||
|
||||
func collectPackRequests(args []string, listFile, defaultRevision string) ([]packRequest, error) {
|
||||
// If a list file is specified
|
||||
// for example, "pack -list packages.go.mod"
|
||||
if listFile != "" {
|
||||
if len(args) != 0 {
|
||||
return nil, fmt.Errorf("-list cannot be combined with package arguments")
|
||||
}
|
||||
return parsePackListFile(listFile)
|
||||
}
|
||||
|
||||
// if no list file is specified
|
||||
// for example, "pack golang.org/x/a@v0.30.0 golang.org/x/b@v0.7.0"
|
||||
var requests []packRequest
|
||||
for _, arg := range args {
|
||||
gopkg, revision, err := parsePackArg(arg, defaultRevision)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse package argument %q: %w", arg, err)
|
||||
}
|
||||
requests = append(requests, packRequest{arg: arg, gopkg: gopkg, revision: revision})
|
||||
}
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
func parsePackListFile(filename string) ([]packRequest, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %q: %w", filename, err)
|
||||
}
|
||||
f, err := modfile.Parse(filename, data, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse %q: %w", filename, err)
|
||||
}
|
||||
|
||||
// TODO: Support go.mod directives that affect module version selection,
|
||||
// such as replace, exclude, and retract, before accepting them in pack lists.
|
||||
if len(f.Replace) > 0 || len(f.Exclude) > 0 || len(f.Retract) > 0 {
|
||||
return nil, fmt.Errorf("%q contains unsupported directives", filename)
|
||||
}
|
||||
if len(f.Require) == 0 {
|
||||
return nil, fmt.Errorf("%q contains no require directives", filename)
|
||||
}
|
||||
|
||||
requests := make([]packRequest, 0, len(f.Require))
|
||||
for _, require := range f.Require {
|
||||
// TODO: Decide whether indirect requirements should be included in pack lists.
|
||||
if err := module.Check(require.Mod.Path, require.Mod.Version); err != nil {
|
||||
return nil, fmt.Errorf("invalid require %q %q: %w",
|
||||
require.Mod.Path, require.Mod.Version, err)
|
||||
}
|
||||
requests = append(requests, packRequest{
|
||||
arg: require.Mod.Path + "@" + require.Mod.Version,
|
||||
gopkg: require.Mod.Path,
|
||||
revision: require.Mod.Version,
|
||||
})
|
||||
}
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
func preflightPackRequests(ctx context.Context, requests []packRequest, pkgType packageType, customProgPkgName string, allowUnknownHoster bool) error {
|
||||
seenOutputDirs := make(map[string]string)
|
||||
for _, req := range requests {
|
||||
gopkg := strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(req.gopkg, "https://"), "http://"), "git://")
|
||||
info, err := getPkgsiteInfo(ctx, gopkg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%q: %w", req.arg, err)
|
||||
}
|
||||
if repoURL := gitCloneURLFromRepoURL(info.Module.RepoURL); repoURL == "" {
|
||||
return fmt.Errorf("%q: no repository URL", req.arg)
|
||||
}
|
||||
if req.revision != "" && strings.HasPrefix(req.revision, "v") && !module.IsPseudoVersion(req.revision) {
|
||||
if _, err := getPkgsiteModule(ctx, info.Module.Path, req.revision); err != nil {
|
||||
return fmt.Errorf("%q@%s: %w", req.arg, req.revision, err)
|
||||
}
|
||||
}
|
||||
|
||||
effectivePkgType := pkgType
|
||||
if effectivePkgType == typeGuess {
|
||||
effectivePkgType = typeLibrary
|
||||
if info.Package.Name == "main" {
|
||||
effectivePkgType = typeProgram
|
||||
}
|
||||
}
|
||||
dir := nameFromGopkg(gopkg, effectivePkgType, customProgPkgName, allowUnknownHoster)
|
||||
if previous, ok := seenOutputDirs[dir]; ok {
|
||||
return fmt.Errorf("%q and %q use %q", previous, req.arg, dir)
|
||||
}
|
||||
seenOutputDirs[dir] = req.arg
|
||||
dirInfo, err := os.Stat(dir)
|
||||
if err == nil {
|
||||
if !dirInfo.IsDir() {
|
||||
return fmt.Errorf("%q: %q is not a directory", req.arg, dir)
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%q: read %q: %w", req.arg, dir, err)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
return fmt.Errorf("%q: %q is non empty", req.arg, dir)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("%q: stat %q: %w", req.arg, dir, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePackArg(arg, defaultRevision string) (string, string, error) {
|
||||
arg = strings.TrimSpace(arg)
|
||||
defaultRevision = strings.TrimSpace(defaultRevision)
|
||||
if arg == "" {
|
||||
return "", "", fmt.Errorf("package import path is empty")
|
||||
}
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
return "", "", fmt.Errorf("-git_revision must be specified before package arguments")
|
||||
}
|
||||
if at := strings.LastIndex(arg, "@"); at >= 0 {
|
||||
revision := strings.TrimSpace(arg[at+1:])
|
||||
if revision == "" {
|
||||
return "", "", fmt.Errorf("package revision is empty")
|
||||
}
|
||||
path := strings.TrimSpace(arg[:at])
|
||||
if path == "" {
|
||||
return "", "", fmt.Errorf("package import path is empty")
|
||||
}
|
||||
return path, revision, nil
|
||||
}
|
||||
return arg, defaultRevision, nil
|
||||
}
|
||||
|
||||
func packPackage(gopkg, gitRevision string, forcePrerelease bool, pkgType packageType, customProgPkgName string, allowUnknownHoster bool) {
|
||||
// Remove URL scheme if present (https://, http://, git://, etc.)
|
||||
gopkg = strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(gopkg, "https://"), "http://"), "git://")
|
||||
|
||||
// Verify the provided argument using the official pkg.go.dev API. Keep the
|
||||
// requested path for naming and go_import_path; use the module path only for
|
||||
// source checkout and tarball generation.
|
||||
info, err := getPkgsiteInfo(context.TODO(), gopkg)
|
||||
if err != nil {
|
||||
log.Fatalf("Verifying arguments: %v — did you specify a Go package import path?", err)
|
||||
}
|
||||
sourceGopkg := sourceImportPathForPackage(gopkg, info)
|
||||
if sourceGopkg != gopkg {
|
||||
log.Printf("Using module root %q as source checkout for specified import path %q", sourceGopkg, gopkg)
|
||||
}
|
||||
|
||||
// Set default source and binary package names.
|
||||
openRuyiSrc := nameFromGopkg(gopkg, typeLibrary, customProgPkgName, allowUnknownHoster)
|
||||
openRuyiLib := openRuyiSrc
|
||||
openRuyiProgram := nameFromGopkg(gopkg, typeProgram, customProgPkgName, allowUnknownHoster)
|
||||
|
||||
if pkgType != typeGuess {
|
||||
openRuyiSrc = nameFromGopkg(gopkg, pkgType, customProgPkgName, allowUnknownHoster)
|
||||
}
|
||||
|
||||
+8
-4
@@ -358,10 +358,7 @@ func pkgsiteLicenseExpression(licenses []pkgsiteLicense) string {
|
||||
types = append(types, typ)
|
||||
}
|
||||
sort.Strings(types)
|
||||
group := types[0]
|
||||
if len(types) > 1 {
|
||||
group = "(" + strings.Join(types, " OR ") + ")"
|
||||
}
|
||||
group := strings.Join(types, " OR ")
|
||||
seenGroups[group] = true
|
||||
}
|
||||
if len(seenGroups) == 0 {
|
||||
@@ -373,5 +370,12 @@ func pkgsiteLicenseExpression(licenses []pkgsiteLicense) string {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
sort.Strings(groups)
|
||||
if len(groups) > 1 {
|
||||
for i, group := range groups {
|
||||
if strings.Contains(group, " OR ") {
|
||||
groups[i] = "(" + group + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(groups, " AND ")
|
||||
}
|
||||
|
||||
+260
-1
@@ -48,7 +48,15 @@ func TestPkgsiteLicenseExpression(t *testing.T) {
|
||||
licenses: []pkgsiteLicense{
|
||||
{FilePath: "LICENSE", Types: []string{"MIT", "Apache-2.0"}},
|
||||
},
|
||||
want: "(Apache-2.0 OR MIT)",
|
||||
want: "Apache-2.0 OR MIT",
|
||||
},
|
||||
{
|
||||
name: "parenthesizes OR group when joined with AND",
|
||||
licenses: []pkgsiteLicense{
|
||||
{FilePath: "LICENSE", Types: []string{"MIT", "Apache-2.0"}},
|
||||
{FilePath: "COPYING", Types: []string{"BSD-3-Clause"}},
|
||||
},
|
||||
want: "(Apache-2.0 OR MIT) AND BSD-3-Clause",
|
||||
},
|
||||
{
|
||||
name: "uses TODO when pkgsite has no SPDX type",
|
||||
@@ -555,6 +563,257 @@ func TestPkgVersionFromGitUsesPackagingDateAndSevenCharHash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPkgVersionFromGitAcceptsUnprefixedSemverPreferredRevision(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
runGit(t, dir, nil, "init")
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/project\n"), 0644); err != nil {
|
||||
t.Fatalf("write go.mod: %v", err)
|
||||
}
|
||||
runGit(t, dir, nil, "add", "go.mod")
|
||||
runGit(t, dir, []string{
|
||||
"GIT_AUTHOR_NAME=Test",
|
||||
"GIT_AUTHOR_EMAIL=test@example.invalid",
|
||||
"GIT_COMMITTER_NAME=Test",
|
||||
"GIT_COMMITTER_EMAIL=test@example.invalid",
|
||||
}, "commit", "-m", "initial")
|
||||
runGit(t, dir, nil, "tag", "v1.2.0")
|
||||
|
||||
u := upstream{}
|
||||
got, err := pkgVersionFromGit(dir, &u, "1.2.0", false)
|
||||
if err != nil {
|
||||
t.Fatalf("pkgVersionFromGit() returned error: %v", err)
|
||||
}
|
||||
if got != "1.2.0" {
|
||||
t.Fatalf("pkgVersionFromGit() = %q, want %q", got, "1.2.0")
|
||||
}
|
||||
if u.tag != "v1.2.0" || !u.isRelease {
|
||||
t.Fatalf("tag = %q, isRelease = %v, want v1.2.0 release", u.tag, u.isRelease)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPkgVersionFromGitAcceptsPseudoVersion(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
runGit(t, dir, nil, "init")
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/project\n"), 0644); err != nil {
|
||||
t.Fatalf("write go.mod: %v", err)
|
||||
}
|
||||
runGit(t, dir, nil, "add", "go.mod")
|
||||
runGit(t, dir, []string{
|
||||
"GIT_AUTHOR_NAME=Test",
|
||||
"GIT_AUTHOR_EMAIL=test@example.invalid",
|
||||
"GIT_COMMITTER_NAME=Test",
|
||||
"GIT_COMMITTER_EMAIL=test@example.invalid",
|
||||
}, "commit", "-m", "initial")
|
||||
fullHash := strings.TrimSpace(runGit(t, dir, nil, "rev-parse", "HEAD"))
|
||||
pseudoVersion := "v0.0.0-20260722051018-" + fullHash[:12]
|
||||
|
||||
u := upstream{}
|
||||
got, err := pkgVersionFromGit(dir, &u, pseudoVersion, false)
|
||||
if err != nil {
|
||||
t.Fatalf("pkgVersionFromGit() returned error: %v", err)
|
||||
}
|
||||
if got != strings.TrimPrefix(pseudoVersion, "v") {
|
||||
t.Fatalf("pkgVersionFromGit() = %q, want %q", got, strings.TrimPrefix(pseudoVersion, "v"))
|
||||
}
|
||||
if u.commitIsh != fullHash[:12] || u.isRelease {
|
||||
t.Fatalf("commitIsh = %q, isRelease = %v, want %q and false", u.commitIsh, u.isRelease, fullHash[:12])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackArg(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
defaultRevision string
|
||||
wantPath string
|
||||
wantRevision string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "explicit revision",
|
||||
arg: "github.com/a/pkg@v1.2.3",
|
||||
wantPath: "github.com/a/pkg",
|
||||
wantRevision: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "explicit unprefixed semver revision",
|
||||
arg: "github.com/a/pkg@1.2.3",
|
||||
wantPath: "github.com/a/pkg",
|
||||
wantRevision: "1.2.3",
|
||||
},
|
||||
{
|
||||
name: "uses default revision",
|
||||
arg: "github.com/a/pkg",
|
||||
defaultRevision: "v0.9.0",
|
||||
wantPath: "github.com/a/pkg",
|
||||
wantRevision: "v0.9.0",
|
||||
},
|
||||
{
|
||||
name: "keeps empty default revision",
|
||||
arg: "github.com/a/pkg",
|
||||
wantPath: "github.com/a/pkg",
|
||||
},
|
||||
{
|
||||
name: "semantic import version with explicit revision",
|
||||
arg: "github.com/a/pkg/v2@v2.1.0",
|
||||
wantPath: "github.com/a/pkg/v2",
|
||||
wantRevision: "v2.1.0",
|
||||
},
|
||||
{
|
||||
name: "rejects empty explicit revision",
|
||||
arg: "github.com/a/pkg@",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects empty import path",
|
||||
arg: "@v1.2.3",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects misplaced flag",
|
||||
arg: "-list",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects misplaced git revision assignment",
|
||||
arg: "-git_revision=v1.2.3",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotPath, gotRevision, err := parsePackArg(tt.arg, tt.defaultRevision)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("parsePackArg() returned error: %v", err)
|
||||
}
|
||||
if gotPath != tt.wantPath || gotRevision != tt.wantRevision {
|
||||
t.Fatalf("parsePackArg() = (%q, %q), want (%q, %q)", gotPath, gotRevision, tt.wantPath, tt.wantRevision)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackListFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
listPath := filepath.Join(dir, "packages.go.mod")
|
||||
content := strings.Join([]string{
|
||||
"module example.com/package-list",
|
||||
"",
|
||||
"go 1.25",
|
||||
"",
|
||||
"require (",
|
||||
" github.com/a/pkg v1.2.3",
|
||||
" github.com/b/tool v0.4.5 // indirect",
|
||||
")",
|
||||
"",
|
||||
"require golang.org/x/text v0.31.0",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(listPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write list: %v", err)
|
||||
}
|
||||
|
||||
got, err := parsePackListFile(listPath)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePackListFile() returned error: %v", err)
|
||||
}
|
||||
want := []packRequest{
|
||||
{arg: "github.com/a/pkg@v1.2.3", gopkg: "github.com/a/pkg", revision: "v1.2.3"},
|
||||
{arg: "github.com/b/tool@v0.4.5", gopkg: "github.com/b/tool", revision: "v0.4.5"},
|
||||
{arg: "golang.org/x/text@v0.31.0", gopkg: "golang.org/x/text", revision: "v0.31.0"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("len(parsePackListFile()) = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("parsePackListFile()[%d] = %#v, want %#v", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackListFileRejectsUnsupportedDirectives(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
listPath := filepath.Join(dir, "packages.go.mod")
|
||||
content := strings.Join([]string{
|
||||
"module example.com/package-list",
|
||||
"",
|
||||
"go 1.25",
|
||||
"require github.com/a/pkg v1.2.3",
|
||||
"replace github.com/a/pkg => ./pkg",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(listPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write list: %v", err)
|
||||
}
|
||||
|
||||
_, err := parsePackListFile(listPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected parsePackListFile() to reject replace directives")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupported directives") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackListFileRejectsEmptyRequireList(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
listPath := filepath.Join(dir, "packages.go.mod")
|
||||
content := strings.Join([]string{
|
||||
"module example.com/package-list",
|
||||
"",
|
||||
"go 1.25",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(listPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write list: %v", err)
|
||||
}
|
||||
|
||||
_, err := parsePackListFile(listPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected parsePackListFile() to reject an empty require list")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no require directives") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectPackRequestsUsesListFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
listPath := filepath.Join(dir, "packages.go.mod")
|
||||
content := strings.Join([]string{
|
||||
"module example.com/package-list",
|
||||
"",
|
||||
"go 1.25",
|
||||
"require github.com/a/pkg v1.2.3",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(listPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write list: %v", err)
|
||||
}
|
||||
|
||||
got, err := collectPackRequests(nil, listPath, "v9.9.9")
|
||||
if err != nil {
|
||||
t.Fatalf("collectPackRequests() returned error: %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].gopkg != "github.com/a/pkg" || got[0].revision != "v1.2.3" {
|
||||
t.Fatalf("collectPackRequests() = %#v, want github.com/a/pkg@v1.2.3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectPackRequestsRejectsListAndPackages(t *testing.T) {
|
||||
_, err := collectPackRequests([]string{"github.com/a/pkg@v1.2.3"}, "packages.go.mod", "")
|
||||
if err == nil {
|
||||
t.Fatal("expected collectPackRequests() to reject -list combined with package arguments")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "-list cannot be combined") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runGit(t *testing.T, dir string, env []string, args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
|
||||
@@ -196,17 +196,18 @@ func writeSpec(dir, gopkg, openRuyiSrc, openRuyiLib, openRuyiProgram, version st
|
||||
log.Fatalf("Invalid pkgType %d in writeRPMSpec(), aborting", pkgType)
|
||||
}
|
||||
|
||||
fmt.Fprintf(f, "\n")
|
||||
if pkgType != typeLibrary {
|
||||
fmt.Fprintf(f, "\n")
|
||||
}
|
||||
fmt.Fprintf(f, "%%description\n")
|
||||
fmt.Fprintf(f, "%s\n", longdescription)
|
||||
fmt.Fprintf(f, "\n")
|
||||
fmt.Fprintf(f, "%s\n\n", strings.TrimRight(longdescription, "\n"))
|
||||
|
||||
// %files
|
||||
writeRPMFilesSection(f, openRuyiSrc, openRuyiLib, openRuyiProgram, pkgType, assetFiles)
|
||||
|
||||
// %changelog
|
||||
fmt.Fprintf(f, "%%changelog\n")
|
||||
fmt.Fprintf(f, "%%{?autochangelog}\n\n")
|
||||
fmt.Fprintf(f, "%%autochangelog\n\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user