Init project

This commit is contained in:
2025-12-01 14:27:48 +08:00
commit 2ace98388d
10 changed files with 1815 additions and 0 deletions

169
check_version.go Normal file
View File

@@ -0,0 +1,169 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"regexp"
"slices"
"strconv"
"strings"
"time"
"unicode"
)
var (
// describeRegexp parses the count and revision part of the “git describe --long” output.
describeRegexp = regexp.MustCompile(`-\d+-g([0-9a-f]+)\s*$`)
// semverRegexp checks if a string is a valid Go semver,
// from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
// with leading "v" added.
semverRegexp = regexp.MustCompile(`^v(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?: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<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
// uversionPrereleaseRegexp checks for upstream pre-release
// so that '-' can be replaced with '~' in pkgVersionFromGit.
// To be kept in sync with the regexp portion of uversionmanglePattern in spec.go
uversionPrereleaseRegexp = regexp.MustCompile(`(\d)[_\.\-\+]?(RC|rc|pre|dev|beta|alpha)[.]?(\d*)$`)
)
// pkgVersionFromGit determines the actual version to be packaged
// from the git repository status and user preference.
// Besides returning the upstream version, the "upstream" struct
// struct fields u.version, u.commitIsh, u.hasRelease and u.isRelease
// are also set.
// `preferredRev` should be empty if there are no user preferences.
// TODO: also support other VCS
func pkgVersionFromGit(gitdir string, u *upstream, preferredRev string, forcePrerelease bool) (string, error) {
var latestTag string
var commitsAhead int
var cmd *exec.Cmd // the temporary shell commands we execute
// If the user specifies a valid tag as the preferred revision, that tag should be used without additional heuristics.
if u.rr != nil {
if out, err := u.rr.VCS.Tags(gitdir); err == nil && slices.Contains(out, preferredRev) {
latestTag = preferredRev
}
}
// Find @latest version tag (whether annotated or not) when the user
// (1) does not specify a version tag, or
// (2) specifies an invalid version tag.
if len(latestTag) == 0 {
cmd = exec.Command("git", "describe", "--abbrev=0", "--tags", "--exclude", "*/v*")
cmd.Dir = gitdir
if out, err := cmd.Output(); err == nil {
latestTag = strings.TrimSpace(string(out))
}
}
if len(latestTag) > 0 {
u.hasRelease = true
u.tag = latestTag
log.Printf("Found latest tag %q", latestTag)
if !semverRegexp.MatchString(latestTag) {
log.Printf("WARNING: Latest tag %q is not a valid SemVer version\n", latestTag)
// TODO: Enforce strict sementic versioning with leading "v"?
}
// Count number of commits since @latest version
cmd = exec.Command("git", "rev-list", "--count", latestTag+"..HEAD")
cmd.Dir = gitdir
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git rev-list: %w", err)
}
commitsAhead, err = strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return "", fmt.Errorf("parse commits ahead: %w", err)
}
if commitsAhead == 0 {
// Equivalent to "git describe --exact-match --tags"
log.Printf("Latest tag %q matches master", latestTag)
} else {
log.Printf("INFO: master is ahead of %q by %v commits", latestTag, commitsAhead)
}
u.commitIsh = latestTag
// Mangle latestTag into upstream_version
// TODO: Move to function and write unit test?
u.version = strings.TrimLeftFunc(
uversionPrereleaseRegexp.ReplaceAllString(latestTag, "$1~$2$3"),
func(r rune) bool {
return !unicode.IsNumber(r)
},
)
if forcePrerelease {
log.Printf("INFO: Force packaging master (prerelease) as requested by user")
// Fallthrough to package @master (prerelease)
} else {
u.isRelease = true
return u.version, nil
}
}
// Packaging @master (prerelease)
mainVer := ""
//if u.hasRelease {
// mainVer = u.version + "+"
//}
// Find committer date, UNIX timestamp
cmd = exec.Command("git", "log", "--pretty=format:%ct", "-n1", "--no-show-signature")
cmd.Dir = gitdir
lastCommitUnixBytes, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git log: %w", err)
}
lastCommitUnix, err := strconv.ParseInt(strings.TrimSpace(string(lastCommitUnixBytes)), 0, 64)
if err != nil {
return "", fmt.Errorf("parse last commit date: %w", err)
}
dateStr := time.Unix(lastCommitUnix, 0).UTC().Format("20060102")
// Fetch commit hash
cmd = exec.Command("git", "describe", "--long", "--tags")
cmd.Dir = gitdir
lastCommitHash := ""
describeBytes, err := cmd.Output()
if err != nil {
// In case there are no tags at all, we just use the sha of the current commit
cmd = exec.Command("git", "rev-parse", "--short", "HEAD")
cmd.Dir = gitdir
cmd.Stderr = os.Stderr
revparseBytes, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git rev-parse: %w", err)
}
lastCommitHash = strings.TrimSpace(string(revparseBytes))
u.commitIsh = lastCommitHash
} else {
submatches := describeRegexp.FindSubmatch(describeBytes)
if submatches == nil {
return "", fmt.Errorf("git describe output %q does not match expected format", string(describeBytes))
}
lastCommitHash = string(submatches[1])
u.commitIsh = strings.TrimSpace(string(describeBytes))
}
u.version = fmt.Sprintf("%sgit%s.%s",
mainVer,
time.Unix(lastCommitUnix, 0).UTC().Format("20060102"),
lastCommitHash)
if u.hasRelease {
// have tag: 1.2.3.20250101+git9f107c8
u.version = fmt.Sprintf("%s.%s+git%s", u.version, dateStr, lastCommitHash)
} else {
// without tag: 20250101+git9f107c8
u.version = fmt.Sprintf("%s+git%s", dateStr, lastCommitHash)
}
return u.version, nil
}

99
description.go Normal file
View File

@@ -0,0 +1,99 @@
package main
import (
"context"
_ "embed"
"fmt"
"regexp"
"strings"
"github.com/charmbracelet/glamour"
)
//go:embed description.json
var descriptionJSONBytes []byte
// reformatForControl reformats the wrapped description
// to conform to Debians control format.
func reformatForControl(raw string) string {
output := ""
next_prefix := ""
re := regexp.MustCompile(`^ \d+\. `)
for _, line := range strings.Split(strings.TrimSpace(raw), "\n") {
// Remove paddings that Glamour currently add to the end of each line
line = strings.TrimRight(line, " ")
// Try to add hanging indent for list items that span over one line
prefix := next_prefix
if strings.HasPrefix(line, " * ") {
// unordered list
prefix = ""
next_prefix = " "
}
if re.MatchString(line) {
// ordered list
prefix = ""
next_prefix = " "
}
if line == "" {
// blank line, implying end of list
line = "."
prefix = ""
next_prefix = ""
}
output += " " + prefix + line + "\n"
}
return output
}
// markdownToLongDescription converts Markdown to plain text
// and reformat it for expanded description in debian/control.
func markdownToLongDescription(markdown string) (string, error) {
r, _ := glamour.NewTermRenderer(
glamour.WithStylesFromJSONBytes(descriptionJSONBytes),
glamour.WithWordWrap(72),
)
out, err := r.Render(markdown)
if err != nil {
return "", fmt.Errorf("fail to render Markdown: %w", err)
}
//fmt.Println(out)
//fmt.Println(reformatForControl(out))
return reformatForControl(out), nil
}
// getDescriptionForGopkg reads from README.md (or equivalent) from GitHub,
// intended for extended description in debian/control.
func getLongDescriptionForGopkg(gopkg string) (string, error) {
owner, repo, err := findGitHubRepo(gopkg)
if err != nil {
return "", fmt.Errorf("find github repo: %w", err)
}
rr, _, err := gitHub.Repositories.GetReadme(context.TODO(), owner, repo, nil)
if err != nil {
return "", fmt.Errorf("get readme: %w", err)
}
content, err := rr.GetContent()
if err != nil {
return "", fmt.Errorf("get content: %w", err)
}
// Supported filename suffixes are from
// https://github.com/github/markup/blob/master/README.md
// NOTE(stapelberg): Ideally, wed use https://github.com/github/markup
// itself to render to HTML, then convert HTML to plaintext. That sounds
// fairly involved, but itd be the most correct solution to the problem at
// hand. Our current code just knows markdown, which is good enough since
// most (Go?) projects in fact use markdown for their README files.
if !strings.HasSuffix(rr.GetName(), "md") &&
!strings.HasSuffix(rr.GetName(), "markdown") &&
!strings.HasSuffix(rr.GetName(), "mdown") &&
!strings.HasSuffix(rr.GetName(), "mkdn") {
return reformatForControl(content), nil
}
return markdownToLongDescription(content)
}

96
description.json Normal file
View File

@@ -0,0 +1,96 @@
{
"document": {
"block_prefix": "\n",
"block_suffix": "\n",
"margin": 0
},
"block_quote": {
"indent": 1,
"indent_token": " | "
},
"paragraph": {},
"list": {
"indent": 1,
"level_indent": 4
},
"heading": {
"block_suffix": "\n"
},
"h1": {
"prefix": ""
},
"h2": {
"prefix": ""
},
"h3": {
"prefix": ""
},
"h4": {
"prefix": ""
},
"h5": {
"prefix": ""
},
"h6": {
"prefix": ""
},
"text": {},
"strikethrough": {
"block_prefix": "~~",
"block_suffix": "~~"
},
"emph": {
"block_prefix": "*",
"block_suffix": "*"
},
"strong": {
"block_prefix": "**",
"block_suffix": "**"
},
"hr": {
"format": "\n------------------------------------------------------------------------\n"
},
"item": {
"block_prefix": "* ",
"indent": 2
},
"enumeration": {
"block_prefix": ". ",
"indent": 2
},
"task": {
"ticked": "[x] ",
"unticked": "[ ] "
},
"link": {
"prefix": "(",
"suffix": ")"
},
"link_text": {},
"image": {
"prefix": "(",
"suffix": ")"
},
"image_text": {
"format": "[Image: {{.text}}]"
},
"code": {
"block_prefix": "",
"block_suffix": ""
},
"code_block": {
"margin": 2
},
"table": {
"center_separator": "+",
"column_separator": "|",
"row_separator": "-"
},
"definition_list": {},
"definition_term": {},
"definition_description": {
"block_prefix": "\n* "
},
"html_block": {},
"html_span": {}
}

39
go.mod Normal file
View File

@@ -0,0 +1,39 @@
module go2spec
go 1.25.3
require (
github.com/charmbracelet/glamour v0.10.0
github.com/google/go-github/v60 v60.0.0
github.com/mattn/go-isatty v0.0.20
github.com/sandrolain/httpcache v1.4.0
golang.org/x/net v0.47.0
golang.org/x/tools/go/vcs v0.1.0-deprecated
)
require (
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
)

81
go.sum Normal file
View File

@@ -0,0 +1,81 @@
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8=
github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sandrolain/httpcache v1.4.0 h1:Jf4Vx62X2ybvNPSpPvI1kT3xvMdDG1AsApQjOQKO9E0=
github.com/sandrolain/httpcache v1.4.0/go.mod h1:kHBuXveitSn39SNPBhdf/ybG272X706HJ2RJqOQ+Em0=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4=
golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

61
main.go Normal file
View File

@@ -0,0 +1,61 @@
package main
import (
"os"
"github.com/google/go-github/v60/github"
"github.com/sandrolain/httpcache"
)
var (
gitHub *github.Client
)
func printHelp() {
helpText := `go2spec - A tool to package Go modules into RPM spec files.
Usage:
go2spec [command]
Available Commands:
pack Package the Go modules into an RPM spec file
help Show this help message
Use "go2spec [command] --help" for more information about a command.
If there are no commands provided, the tool will default to executing the 'pack' command.
`
println(helpText)
}
func main() {
transport := github.BasicAuthTransport{
Username: os.Getenv("GITHUB_USERNAME"),
Password: os.Getenv("GITHUB_PASSWORD"),
OTP: os.Getenv("GITHUB_OTP"),
Transport: httpcache.NewMemoryCacheTransport(),
}
gitHub = github.NewClient(transport.Client())
args := os.Args[1:]
cmd := ""
if len(args) > 0 {
cmd = args[0]
}
switch cmd {
case "help":
printHelp()
case "pack":
// Placeholder for the pack command implementation
println("Executing 'pack' command...")
// Actual packing logic would go here
mainPack(args[1:], nil)
default:
// Default to 'pack' command if no command is provided
println("No command provided. Defaulting to 'pack' command...")
// Actual packing logic would go here
mainPack(args, usage)
}
}

130
metadata.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"golang.org/x/net/html"
)
// To update, use:
// curl -s https://api.github.com/licenses | jq '.[].key'
var githubLicenseToSPDXLicense = map[string]string{
//"agpl-3.0"
"apache-2.0": "Apache-2.0",
"artistic-2.0": "Artistic-2.0",
"bsd-2-clause": "BSD-2-Clause",
"bsd-3-clause": "BSD-3-Clause",
"cc0-1.0": "CC0-1.0",
//"epl-1.0" (eclipse public license)
"gpl-2.0": "GPL-2.0-only",
"gpl-3.0": "GPL-3.0-only",
"isc": "ISC",
"lgpl-2.1": "LGPL-2.1-only",
"lgpl-3.0": "LGPL-3.0-only",
"mit": "MIT",
"mpl-2.0": "MPL-2.0",
//"unlicense"
}
var githubRegexp = regexp.MustCompile(`github\.com/([^/]+/[^/]+)`)
func findGitHubOwnerRepo(gopkg string) (string, error) {
if strings.HasPrefix(gopkg, "github.com/") {
return strings.TrimPrefix(gopkg, "github.com/"), nil
}
resp, err := http.Get("https://" + gopkg + "?go-get=1")
if err != nil {
return "", fmt.Errorf("HTTP get: %w", err)
}
defer resp.Body.Close()
z := html.NewTokenizer(resp.Body)
for {
tt := z.Next()
if tt == html.ErrorToken {
return "", fmt.Errorf("%q is not on GitHub", gopkg)
}
token := z.Token()
if token.Data != "meta" {
continue
}
var meta struct {
name, content string
}
for _, attr := range token.Attr {
if attr.Key == "name" {
meta.name = attr.Val
}
if attr.Key == "content" {
meta.content = attr.Val
}
}
match := func(name string, length int) string {
if f := strings.Fields(meta.content); meta.name == name && len(f) == length {
if f[0] != gopkg {
return ""
}
if repoMatch := githubRegexp.FindStringSubmatch(f[2]); repoMatch != nil {
return strings.TrimSuffix(repoMatch[1], ".git")
}
}
return ""
}
if repo := match("go-import", 3); repo != "" {
return repo, nil
}
if repo := match("go-source", 4); repo != "" {
return repo, nil
}
}
}
func findGitHubRepo(gopkg string) (owner string, repo string, _ error) {
ownerrepo, err := findGitHubOwnerRepo(gopkg)
if err != nil {
return "", "", fmt.Errorf("find GitHub owner repo: %w", err)
}
parts := strings.Split(ownerrepo, "/")
if got, want := len(parts), 2; got != want {
return "", "", fmt.Errorf("invalid GitHub repo: %q does not follow owner/repo", repo)
}
return parts[0], parts[1], nil
}
func getLicenseForGopkg(gopkg string) (string, error) {
owner, repo, err := findGitHubRepo(gopkg)
if err != nil {
return "", fmt.Errorf("find GitHub repo: %w", err)
}
rl, _, err := gitHub.Repositories.License(context.TODO(), owner, repo)
if err != nil {
return "", fmt.Errorf("get license for Go package: %w", err)
}
if license, ok := githubLicenseToSPDXLicense[rl.GetLicense().GetKey()]; ok {
return license, nil
}
return "TODO", nil
}
// getDescriptionForGopkg gets the package description from GitHub,
// intended for the synopsis or the short description in debian/control.
func getDescriptionForGopkg(gopkg string) (string, error) {
owner, repo, err := findGitHubRepo(gopkg)
if err != nil {
return "", fmt.Errorf("find GitHub repo: %w", err)
}
rr, _, err := gitHub.Repositories.Get(context.TODO(), owner, repo)
if err != nil {
return "", err
}
return strings.TrimSpace(rr.GetDescription()), nil
}

742
pack.go Normal file
View File

@@ -0,0 +1,742 @@
package main
import (
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/net/publicsuffix"
"golang.org/x/tools/go/vcs"
)
type packageType int
const (
typeGuess packageType = iota
typeLibrary
typeProgram
typeLibraryProgram
typeProgramLibrary
)
// upstream describes the upstream repo we are about to package.
type upstream struct {
rr *vcs.RepoRoot
tarPath string // path to the downloaded or generated orig tarball tempfile
compression string // compression method, either "gz" or "xz"
version string // upstream version number, e.g. 0.0~git20180204.1d24609
tag string // Latest upstream tag, if any
commitIsh string // commit-ish corresponding to upstream version to be packaged
remote string // git remote, set to short hostname if upstream git history is included
firstMain string // import path of the first main package within repo, if any
vendorDirs []string // all vendor sub directories, relative to the repo directory
repoDeps []string // the repository paths of all dependencies (e.g. github.com/zyedidia/glob)
hasGodeps bool // whether the Godeps/_workspace directory exists
hasRelease bool // whether any release tags exist, for debian/watch
isRelease bool // whether what we end up packaging is a tagged release
}
var errUnsupportedHoster = errors.New("unsupported hoster")
func passthroughEnv() []string {
var relevantVariables = []string{
"HOME",
"PATH",
"HTTP_PROXY", "http_proxy",
"HTTPS_PROXY", "https_proxy",
"ALL_PROXY", "all_proxy",
"NO_PROXY", "no_proxy",
"GIT_PROXY_COMMAND",
"GIT_HTTP_PROXY_AUTHMETHOD",
}
var result []string
for _, variable := range relevantVariables {
if value, ok := os.LookupEnv(variable); ok {
result = append(result, fmt.Sprintf("%s=%s", variable, value))
}
}
return result
}
// findVendorDirs walks the directory tree rooted at dir and returns
// all vendor/ directories found, relative to dir.
func findVendorDirs(dir string) ([]string, error) {
var vendorDirs []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info != nil && !info.IsDir() {
return nil // nothing to do for anything but directories
}
if info.Name() == ".git" ||
info.Name() == ".hg" ||
info.Name() == ".bzr" {
return filepath.SkipDir
}
if info.Name() == "vendor" {
rel, err := filepath.Rel(dir, path)
if err != nil {
return fmt.Errorf("filepath.Rel: %w", err)
}
vendorDirs = append(vendorDirs, rel)
}
return nil
})
return vendorDirs, err
}
func downloadFile(filename, url string) error {
dst, err := os.Create(filename)
if err != nil {
return fmt.Errorf("create: %w", err)
}
defer dst.Close()
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("http get: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("response: %s", resp.Status)
}
_, err = io.Copy(dst, resp.Body)
if err != nil {
return fmt.Errorf("copy: %w", err)
}
return nil
}
// get downloads the specified Go package into the provided GOPATH,
// checking out the specified revision if non-empty.
func (u *upstream) get(gopath, repo, rev string) error {
done := make(chan struct{})
defer close(done)
go progressSize("go get", filepath.Join(gopath, "src"), done)
rr, err := vcs.RepoRootForImportPath(repo, false)
if err != nil {
return fmt.Errorf("get repo root: %w", err)
}
u.rr = rr
dir := filepath.Join(gopath, "src", rr.Root)
if rev != "" {
// Run "git clone {repo} {dir}" and "git checkout {tag}"
return rr.VCS.CreateAtRev(dir, rr.Repo, rev)
}
// Run "git clone {repo} {dir}" (or the equivalent command for hg, svn, bzr)
return rr.VCS.Create(dir, rr.Repo)
}
func (u *upstream) tarballUrl() (string, error) {
repo := strings.TrimSuffix(u.rr.Repo, ".git")
repoU, err := url.Parse(repo)
if err != nil {
return "", fmt.Errorf("parse URL: %w", err)
}
switch repoU.Host {
case "github.com":
return fmt.Sprintf("%s/archive/%s.tar.%s",
repo, u.tag, u.compression), nil
case "gitlab.com", "salsa.debian.org":
parts := strings.Split(repoU.Path, "/")
if len(parts) < 3 {
return "", fmt.Errorf("incomplete repo URL: %s", u.rr.Repo)
}
project := parts[2]
return fmt.Sprintf("%s/-/archive/%s/%s-%s.tar.%s",
repo, u.tag, project, u.tag, u.compression), nil
case "git.sr.ht":
return fmt.Sprintf("%s/archive/%s.tar.%s",
repo, u.tag, u.compression), nil
case "codeberg.org":
return fmt.Sprintf("%s/archive/%s.tar.%s",
repo, u.tag, u.compression), nil
default:
return "", errUnsupportedHoster
}
}
func (u *upstream) tarballFromHoster() error {
tarURL, err := u.tarballUrl()
if err != nil {
return err
}
done := make(chan struct{})
go progressSize("Download", u.tarPath, done)
log.Printf("Downloading %s", tarURL)
err = downloadFile(u.tarPath, tarURL)
close(done)
return err
}
func (u *upstream) tar(gopath, repo string) error {
f, err := os.CreateTemp("", "pack-tmp")
if err != nil {
return fmt.Errorf("create temp file: %w", err)
}
u.tarPath = f.Name()
f.Close()
if u.isRelease {
if u.hasGodeps {
log.Printf("Godeps/_workspace exists, not downloading tarball from hoster.")
} else {
u.compression = "gz"
if err := u.tarballFromHoster(); err == nil {
return nil
} else if err == errUnsupportedHoster {
log.Printf("INFO: Hoster does not provide release tarball\n")
} else {
return fmt.Errorf("tarball from hoster: %w", err)
}
}
}
u.compression = "xz"
base := filepath.Base(repo)
log.Printf("Generating temp tarball as %q\n", u.tarPath)
dir := filepath.Dir(repo)
cmd := exec.Command(
"tar",
"cJf",
u.tarPath,
"--exclude=.git",
"--exclude=Godeps/_workspace",
base)
cmd.Dir = filepath.Join(gopath, "src", dir)
cmd.Stderr = os.Stderr
return cmd.Run()
}
// findMains finds main packages within the repo (useful to auto-detect the
// package type).
func (u *upstream) findMains(gopath, repo string) error {
cmd := exec.Command("go", "list", "-e", "-f", "{{.ImportPath}} {{.Name}}", repo+"/...")
cmd.Dir = filepath.Join(gopath, "src", repo)
cmd.Env = passthroughEnv()
cmd.Stderr = os.Stderr
log.Println("findMains: Running", cmd, "in", cmd.Dir)
out, err := cmd.Output()
if err != nil {
log.Println("WARNING: In findMains:", fmt.Errorf("%q: %w", cmd.Args, err))
log.Printf("Retrying without appending \"/...\" to repo")
cmd = exec.Command("go", "list", "-e", "-f", "{{.ImportPath}} {{.Name}}", repo)
cmd.Dir = filepath.Join(gopath, "src", repo)
cmd.Env = passthroughEnv()
cmd.Stderr = os.Stderr
log.Println("findMains: Running", cmd, "in", cmd.Dir)
out, err = cmd.Output()
if err != nil {
log.Println("WARNING: In findMains:", fmt.Errorf("%q: %w", cmd.Args, err))
}
}
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if strings.Contains(line, "/vendor/") ||
strings.Contains(line, "/Godeps/") ||
strings.Contains(line, "/samples/") ||
strings.Contains(line, "/examples/") ||
strings.Contains(line, "/example/") {
continue
}
if strings.HasSuffix(line, " main") {
u.firstMain = strings.TrimSuffix(line, " main")
break
}
}
return nil
}
func (u *upstream) findDependencies(gopath, repo string) error {
log.Printf("Determining dependencies\n")
cmd := exec.Command("go", "list", "-e", "-f", "{{join .Imports \"\\n\"}}\n{{join .TestImports \"\\n\"}}\n{{join .XTestImports \"\\n\"}}", repo+"/...")
cmd.Dir = filepath.Join(gopath, "src", repo)
cmd.Env = passthroughEnv()
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
log.Println("WARNING: In findDependencies:", fmt.Errorf("%q: %w", cmd.Args, err))
// See https://bugs.debian.org/992610
log.Printf("Retrying without appending \"/...\" to repo")
cmd = exec.Command("go", "list", "-e", "-f", "{{join .Imports \"\\n\"}}\n{{join .TestImports \"\\n\"}}\n{{join .XTestImports \"\\n\"}}", repo)
cmd.Dir = filepath.Join(gopath, "src", repo)
cmd.Env = passthroughEnv()
cmd.Stderr = os.Stderr
out, err = cmd.Output()
if err != nil {
log.Println("WARNING: In findDependencies:", fmt.Errorf("%q: %w", cmd.Args, err))
}
}
godependencies := make(map[string]bool)
for _, p := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if p == "" {
continue // skip separators between import types
}
// Strip packages that are included in the repository we are packaging.
if strings.HasPrefix(p, repo+"/") || p == repo {
continue
}
if p == "C" {
// TODO: maybe parse the comments to figure out C deps from pkg-config files?
} else {
godependencies[p] = true
}
}
if len(godependencies) == 0 {
return nil
}
// Remove all packages which are in the standard lib.
cmd = exec.Command("go", "list", "std")
cmd.Stderr = os.Stderr
cmd.Env = passthroughEnv()
out, err = cmd.Output()
if err != nil {
return fmt.Errorf("go list std: (args: %v): %w", cmd.Args, err)
}
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
delete(godependencies, line)
}
// Resolve all packages to the root of their repository.
roots := make(map[string]bool)
for dep := range godependencies {
rr, err := vcs.RepoRootForImportPath(dep, false)
if err != nil {
log.Printf("Could not determine repo path for import path %q: %v\n", dep, err)
continue
}
roots[rr.Root] = true
}
u.repoDeps = make([]string, 0, len(godependencies))
for root := range roots {
u.repoDeps = append(u.repoDeps, root)
}
return nil
}
// makeUpstreamSourceTarball downloads the specified Go package from the Internet,
// checks out the specified revision, determines the version number, removes
// vendored dependencies, and creates a tarball of the upstream source code.
// It returns an upstream struct describing the downloaded package.
func makeUpstreamSourceTarball(repo, revision string, forcePrerelease bool) (*upstream, error) {
gopath, err := os.MkdirTemp("", "pack-tmp")
if err != nil {
return nil, fmt.Errorf("create tmp dir: %w", err)
}
defer os.RemoveAll(gopath)
repoDir := filepath.Join(gopath, "src", repo)
var u upstream
log.Printf("Downloading %q\n", repo+"/...")
if err := u.get(gopath, repo, revision); err != nil {
return nil, fmt.Errorf("go get: %w", err)
}
// Verify early this repository uses git (we call pkgVersionFromGit later):
if _, err := os.Stat(filepath.Join(repoDir, ".git")); os.IsNotExist(err) {
return nil, fmt.Errorf("not a git repository; This program currently only supports git")
}
if _, err := os.Stat(filepath.Join(repoDir, "debian")); err == nil {
log.Printf("WARNING: ignoring debian/ directory that came with the upstream sources\n")
}
u.vendorDirs, err = findVendorDirs(repoDir)
if err != nil {
return nil, fmt.Errorf("find vendor dirs: %w", err)
}
if len(u.vendorDirs) > 0 {
log.Printf("Deleting upstream vendor/ directories")
for _, dir := range u.vendorDirs {
if err := os.RemoveAll(filepath.Join(repoDir, dir)); err != nil {
return nil, fmt.Errorf("remove all: %w", err)
}
}
}
if _, err := os.Stat(filepath.Join(repoDir, "Godeps", "_workspace")); !os.IsNotExist(err) {
log.Println("Godeps/_workspace detected")
u.hasGodeps = true
}
log.Printf("Determining upstream version number\n")
u.version, err = pkgVersionFromGit(repoDir, &u, revision, forcePrerelease)
if err != nil {
return nil, fmt.Errorf("get package version from Git: %w", err)
}
log.Printf("Package version is %q\n", u.version)
if err := u.findMains(gopath, repo); err != nil {
return nil, fmt.Errorf("find mains: %w", err)
}
if err := u.findDependencies(gopath, repo); err != nil {
return nil, fmt.Errorf("find dependencies: %w", err)
}
if err := u.tar(gopath, repo); err != nil {
return nil, fmt.Errorf("tar: %w", err)
}
return &u, nil
}
func createDirectory(openRuyiSrc string) (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("get cwd: %w", err)
}
dir := filepath.Join(wd, openRuyiSrc)
if err := os.Mkdir(dir, 0755); err != nil {
return "", fmt.Errorf("mkdir: %w", err)
}
return dir, nil
}
// Package names (both source and binary, see Package, Section 5.6.7) must
// consist only of lower case letters (a-z), digits (0-9), plus (+) and minus
// (-) signs, and periods (.). They must be at least two characters long and
// must start with an alphanumeric character.
func normalizePackageName(str string) string {
lowerDigitPlusMinusDot := func(r rune) rune {
switch {
case r >= 'a' && r <= 'z' || '0' <= r && r <= '9':
return r
case r >= 'A' && r <= 'Z':
return r + ('a' - 'A')
case r == '.' || r == '+' || r == '-':
return r
case r == '_':
return '-'
}
return -1
}
safe := strings.Trim(strings.Map(lowerDigitPlusMinusDot, str), "-")
if len(safe) < 2 {
return "TODO"
}
return safe
}
// shortHostName maps a Go package import path to a canonical short hostname
func shortHostName(gopkg string, allowUnknownHoster bool) (host string, err error) {
knownHosts := map[string]string{
// keep the list in alphabetical order
"bazil.org": "bazil",
"bitbucket.org": "bitbucket",
"blitiri.com.ar": "blitiri",
"cloud.google.com": "googlecloud",
"code.google.com": "googlecode",
"codeberg.org": "codeberg",
"filippo.io": "filippo",
"fortio.org": "fortio",
"fyne.io": "fyne",
"git.sr.ht": "sourcehut",
"github.com": "github",
"gitlab.com": "gitlab",
"go.bug.st": "bugst",
"go.cypherpunks.ru": "cypherpunks",
"go.mongodb.org": "mongodb",
"go.opentelemetry.io": "opentelemetry",
"go.step.sm": "step",
"go.uber.org": "uber",
"go4.org": "go4",
"gocloud.dev": "gocloud",
"golang.org": "golang",
"google.golang.org": "google",
"gopkg.in": "gopkg",
"honnef.co": "honnef",
"howett.net": "howett",
"k8s.io": "k8s",
"modernc.org": "modernc",
"pault.ag": "pault",
"rsc.io": "rsc",
"salsa.debian.org": "debian",
"sigs.k8s.io": "k8s-sigs",
"software.sslmate.com": "sslmate",
"zgo.at": "zgoat",
}
parts := strings.Split(gopkg, "/")
fqdn := parts[0]
if host, ok := knownHosts[fqdn]; ok {
return host, nil
}
if !allowUnknownHoster {
return "", fmt.Errorf("unknown hoster %q", fqdn)
}
suffix, _ := publicsuffix.PublicSuffix(fqdn)
host = fqdn[:len(fqdn)-len(suffix)-len(".")]
log.Printf("WARNING: Using %q as canonical hostname for %q. If that is not okay, please file a bug against %s.\n", host, fqdn, os.Args[0])
return host, nil
}
// nameFromGopkg maps a Go package import path to a openRuyi package name.
// e.g. "golang.org/x/text" → "go-golang-x-text".
// This follows https://fedoraproject.org/wiki/PackagingDrafts/Go#Package_Names
func nameFromGopkg(gopkg string, t packageType, customProgPkgName string, allowUnknownHoster bool) string {
parts := strings.Split(gopkg, "/")
if t == typeProgram || t == typeProgramLibrary {
if customProgPkgName != "" {
return normalizePackageName(customProgPkgName)
}
return normalizePackageName(parts[len(parts)-1])
}
host, err := shortHostName(gopkg, allowUnknownHoster)
if err != nil {
log.Fatalf("Cannot derive package name: %v. See -help output for -allow_unknown_hoster\n", err)
}
parts[0] = host
return normalizePackageName("go-" + strings.Join(parts, "-"))
}
func copyFile(src, dest string) error {
input, err := os.Open(src)
if err != nil {
return fmt.Errorf("open: %w", err)
}
defer input.Close()
output, err := os.Create(dest)
if err != nil {
return fmt.Errorf("create: %w", err)
}
if _, err := io.Copy(output, input); err != nil {
return fmt.Errorf("copy: %w", err)
}
return output.Close()
}
// mainPack is the entry point for the "pack" command.
func mainPack(args []string, usage func()) {
flagSet := flag.NewFlagSet("pack", flag.ExitOnError)
if usage != nil {
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, "\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, "\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flagSet.PrintDefaults()
}
}
var gitRevision string
flagSet.StringVar(&gitRevision,
"git_revision",
"",
"git revision (see gitrevisions(7)) of the specified Go package\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 forcePrerelease bool
flagSet.BoolVar(&forcePrerelease,
"force_prerelease",
false,
"Package @master or @tip instead of the latest tagged version")
var pkgTypeString string
flagSet.StringVar(&pkgTypeString,
"type",
"",
"Set package type, one of:\n"+
` * "library" (aliases: "lib", "l", "dev")`+"\n"+
` * "program" (aliases: "prog", "p")`+"\n"+
` * "library+program" (aliases: "lib+prog", "l+p", "both")`+"\n"+
` * "program+library" (aliases: "prog+lib", "p+l", "combined")`)
var customProgPkgName string
flagSet.StringVar(&customProgPkgName,
"program_package_name",
"",
"Override the program package name, and the source package name too\n"+
"when appropriate, e.g. to name github.com/cli/cli as \"gh\"")
var allowUnknownHoster bool
flagSet.BoolVar(&allowUnknownHoster,
"allow_unknown_hoster",
false,
"The pkg-go naming conventions use a canonical identifier for\n"+
"the hostname (see https://go-team.pages.debian.net/packaging.html),\n"+
"and the mapping is hardcoded into dh-make-golang.\n"+
"In case you want to package a Go package living on an unknown hoster,\n"+
"you may set this flag to true and double-check that the resulting\n"+
"package name is sane. Contact pkg-go if unsure.")
// Actual pack starts here
// Parse flags
err := flagSet.Parse(args)
if err != nil {
log.Fatalf("parse args: %v", err)
}
// Check for required positional argument
if flagSet.NArg() < 1 {
flagSet.Usage()
os.Exit(1)
}
gitRevision = strings.TrimSpace(gitRevision)
gopkg := flagSet.Arg(0)
// Verify that the provided argument is a valid Go package import path
rr, err := vcs.RepoRootForImportPath(gopkg, false)
if err != nil {
log.Fatalf("Verifying arguments: %v — did you specify a Go package import path?", err)
}
if gopkg != rr.Root {
log.Printf("Continuing with repository root %q instead of specified import path %q", rr.Root, gopkg)
gopkg = rr.Root
}
// Set default source and binary package names.
openRuyiSrc := nameFromGopkg(gopkg, typeLibrary, customProgPkgName, allowUnknownHoster)
openRuyiLib := openRuyiSrc + "-devel"
openRuyiProgram := nameFromGopkg(gopkg, typeProgram, customProgPkgName, allowUnknownHoster)
var pkgType packageType
switch strings.TrimSpace(pkgTypeString) {
case "", "guess":
pkgType = typeGuess
case "library", "lib", "l", "dev":
pkgType = typeLibrary
case "program", "prog", "p":
pkgType = typeProgram
case "library+program", "lib+prog", "l+p", "both":
// Example packages: go-github-alecthomas-chroma,
// go-github-tdewolff-minify, go-github-spf13-viper
pkgType = typeLibraryProgram
case "program+library", "prog+lib", "p+l", "combined":
// Example package: hugo
pkgType = typeProgramLibrary
default:
log.Fatalf("-type=%q not recognized, aborting\n", pkgTypeString)
}
if pkgType != typeGuess {
openRuyiSrc = nameFromGopkg(gopkg, pkgType, customProgPkgName, allowUnknownHoster)
if _, err := os.Stat(openRuyiSrc); err == nil {
log.Fatalf("Output directory %q already exists, aborting\n", openRuyiSrc)
}
}
if strings.ToLower(gopkg) != gopkg {
// Without -git_revision, specifying the package name in the wrong case
// will lead to two checkouts, i.e. wasting bandwidth. With
// -git_revision, packaging might fail.
//
// In case it turns out that Go package names should never contain any
// uppercase letters, we can just auto-convert the argument.
log.Printf("WARNING: Go package names are case-sensitive. Did you really mean %q instead of %q?\n",
gopkg, strings.ToLower(gopkg))
}
// Create a tarball of the upstream source
u, err := makeUpstreamSourceTarball(gopkg, gitRevision, forcePrerelease)
if err != nil {
log.Fatalf("Could not create a tarball of the upstream source: %v\n", err)
}
if pkgType == typeGuess {
if u.firstMain != "" {
log.Printf("Assuming you are packaging a program (because %q defines a main package), use -type to override\n", u.firstMain)
pkgType = typeProgram
openRuyiSrc = nameFromGopkg(gopkg, pkgType, customProgPkgName, allowUnknownHoster)
} else {
pkgType = typeLibrary
}
}
if _, err := os.Stat(openRuyiSrc); err == nil {
log.Fatalf("Output directory %q already exists, aborting\n", openRuyiSrc)
}
orig := fmt.Sprintf("%s_%s.orig.tar.%s", openRuyiSrc, u.version, u.compression)
log.Printf("Moving tempfile to %q\n", orig)
// We need to copy the file, merely renaming is not enough since the file
// might be on a different filesystem (/tmp often is a tmpfs).
if err := copyFile(u.tarPath, orig); err != nil {
log.Fatalf("Could not rename orig tarball from %q to %q: %v\n", u.tarPath, orig, err)
}
if err := os.Remove(u.tarPath); err != nil {
log.Printf("Could not remove tempfile %q: %v\n", u.tarPath, err)
}
// Now we create our rpm spec file from the gathered information.
dir, err := createDirectory(openRuyiSrc)
if err != nil {
log.Fatalf("Could not create repository: %v\n", err)
}
seen := make(map[string]bool)
pkgdependencies := make([]string, 0, len(u.repoDeps))
for _, dep := range u.repoDeps {
pkgname := nameFromGopkg(dep, typeLibrary, "", allowUnknownHoster) + "-dev"
if !seen[pkgname] {
seen[pkgname] = true
pkgdependencies = append(pkgdependencies, pkgname)
}
}
// optional: sort.Strings(debdependencies)
if err := writeSpec(dir, gopkg, openRuyiSrc, openRuyiLib, openRuyiProgram, u.version,
pkgType, pkgdependencies, u); err != nil {
log.Fatalf("Could not create spec file: %v\n", err)
}
log.Println("Done!")
fmt.Printf("\n")
fmt.Printf("Packaging successfully created in %s\n", dir)
switch pkgType {
case typeLibrary:
fmt.Printf(" Binary: %s\n", openRuyiLib)
case typeProgram:
fmt.Printf(" Binary: %s\n", openRuyiProgram)
case typeLibraryProgram:
fmt.Printf(" Binary: %s\n", openRuyiLib)
fmt.Printf(" Binary: %s\n", openRuyiProgram)
case typeProgramLibrary:
fmt.Printf(" Binary: %s\n", openRuyiProgram)
fmt.Printf(" Binary: %s\n", openRuyiLib)
}
fmt.Printf("\n")
}

58
progress.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"fmt"
"github.com/mattn/go-isatty"
"os"
"path/filepath"
"strings"
"time"
)
const (
_ = 1 << (10 * iota)
Kibi
Mebi
Gibi
Tebi
)
func humanizeBytes(b int64) string {
if b > Tebi {
return fmt.Sprintf("%.2f TiB", float64(b)/float64(Tebi))
} else if b > Gibi {
return fmt.Sprintf("%.2f GiB", float64(b)/float64(Gibi))
} else if b > Mebi {
return fmt.Sprintf("%.2f MiB", float64(b)/float64(Mebi))
} else {
return fmt.Sprintf("%.2f KiB", float64(b)/float64(Kibi))
}
}
func progressSize(prefix, path string, done chan struct{}) {
// previous holds how many bytes the previous line contained
// so that we can clear it in its entirety.
var previous int
tty := isatty.IsTerminal(os.Stdout.Fd())
for {
if tty {
var usage int64
filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err == nil && info.Mode().IsRegular() {
usage += info.Size()
}
return nil
})
fmt.Printf("\r%s", strings.Repeat(" ", previous))
previous, _ = fmt.Printf("\r%s: %s", prefix, humanizeBytes(usage))
}
select {
case <-done:
fmt.Printf("\r")
return
case <-time.After(250 * time.Millisecond):
break
}
}
}

340
spec.go Normal file
View File

@@ -0,0 +1,340 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
func writeSpec(dir, gopkg, openRuyiSrc, openRuyiLib, openRuyiProgram, version string,
pkgType packageType, dependencies []string, u *upstream) error {
f, err := os.Create(filepath.Join(dir, "", openRuyiSrc+".spec"))
if err != nil {
return err
}
defer f.Close()
description, err := getDescriptionForGopkg(gopkg)
if err != nil {
log.Printf("Could not determine description for %q: %v\n", gopkg, err)
description = "TODO: short description"
}
longdescription, err := getLongDescriptionForGopkg(gopkg)
if err != nil {
log.Printf("Could not determine long description for %q: %v\n", gopkg, err)
longdescription = "TODO: long description"
}
longdescription = convertLongDescriptionForRPM(longdescription)
license, err := getLicenseForGopkg(gopkg)
if err != nil {
log.Printf("Could not determine license for %q: %v\n", gopkg, err)
license = "TODO"
}
upstreamName := filepath.Base(gopkg)
if regexp.MustCompile(`^v\d+$`).MatchString(upstreamName) {
upstreamName = filepath.Base(filepath.Dir(gopkg))
}
owner, repo, err := findGitHubRepo(gopkg)
if err != nil {
owner = "TODO"
repo = "TODO"
}
// Write the spec file content
// Macros
fmt.Fprintf(f, "%%define _name %s\n", upstreamName)
fmt.Fprintf(f, "%%define go_import_path %s\n", gopkg)
fmt.Fprintf(f, "\n")
// Header
fmt.Fprintf(f, "Name: %s\n", openRuyiSrc)
fmt.Fprintf(f, "Version: %s\n", version)
fmt.Fprintf(f, "Release: %autorelease\n")
fmt.Fprintf(f, "Summary: %s\n", description)
fmt.Fprintf(f, "License: %s\n", license)
fmt.Fprintf(f, "URL: https://github.com/%s/%s\n", owner, repo)
fmt.Fprintf(f, "Source0: https://github.com/%s/%s/archive/v%%{version}.tar.gz#/%%{_name}-%%{version}.tar. gz\n", owner, repo)
fmt.Fprintf(f, "BuildRequires: go\n")
// And other BuildRequires from dependencies
rpmDeps := convertDependenciesToRPM(dependencies)
sort.Strings(rpmDeps)
for _, dep := range rpmDeps {
fmt.Fprintf(f, "BuildRequires: %s\n", dep)
}
fmt.Fprintf(f, "\n")
// For different package types, write different sections
switch pkgType {
case typeLibrary:
writeRPMLibraryPackage(f, gopkg, openRuyiLib, longdescription, rpmDeps)
//case typeProgram:
// TODO: what can this be used for? ExclusiveArch %%{go_arches}?
// writeRPMProgramPackage(f, gopkg, openRuyiProgram, longdescription)
case typeLibraryProgram:
writeRPMLibraryPackage(f, gopkg, openRuyiLib, longdescription, rpmDeps)
writeRPMProgramSubpackage(f, gopkg, openRuyiProgram, openRuyiSrc, description)
case typeProgramLibrary:
//writeRPMProgramPackage(f, gopkg, openRuyiProgram, longdescription)
writeRPMLibrarySubpackage(f, gopkg, openRuyiLib, openRuyiSrc, longdescription, rpmDeps)
default:
log.Fatalf("Invalid pkgType %d in writeRPMSpec(), aborting", pkgType)
}
fmt.Fprintf(f, "\n")
fmt.Fprintf(f, "%%description\n")
fmt.Fprintf(f, "%s\n", longdescription)
fmt.Fprintf(f, "\n")
// %prep
fmt.Fprintf(f, "%%prep\n")
fmt.Fprintf(f, "%%autosetup -n %%{_name}-%%{version}\n")
if len(u.vendorDirs) > 0 {
fmt.Fprintf(f, "# Remove bundled dependencies\n")
for _, vdir := range u.vendorDirs {
fmt.Fprintf(f, "rm -rf %s\n", vdir)
}
}
if u.hasGodeps {
fmt.Fprintf(f, "rm -rf Godeps/_workspace\n")
}
fmt.Fprintf(f, "\n")
// %build
fmt.Fprintf(f, "%%build\n")
switch pkgType {
case typeLibrary:
fmt.Fprintf(f, "# Library package - no build needed\n")
case typeProgram, typeLibraryProgram, typeProgramLibrary:
fmt.Fprintf(f, "export GOPATH=%%{_builddir}/go\n")
fmt.Fprintf(f, "export GOFLAGS=\"-buildmode=pie -trimpath -mod=readonly -modcacherw\"\n")
fmt.Fprintf(f, "export CGO_CFLAGS=\"${CFLAGS:-%%optflags}\"\n")
fmt.Fprintf(f, "export CGO_CXXFLAGS=\"${CXXFLAGS:-%%optflags}\"\n")
fmt.Fprintf(f, "export CGO_LDFLAGS=\"${LDFLAGS:-%%__global_ldflags}\"\n")
fmt.Fprintf(f, "go build -v -o %%{_name} .\n")
}
fmt.Fprintf(f, "\n")
// %install
fmt.Fprintf(f, "%%install\n")
switch pkgType {
case typeLibrary:
fmt.Fprintf(f, "# Install source code for library package\n")
fmt.Fprintf(f, "install -d %%{buildroot}%%{_datadir}/gocode/src/%%{go_import_path}\n")
fmt.Fprintf(f, "cp -pav *. go go. mod go.sum %%{buildroot}%%{_datadir}/gocode/src/%%{go_import_path}/ || :\n")
fmt.Fprintf(f, "find . -mindepth 1 -type d -not -path './.*' -exec install -d %%{buildroot}%%{_datadir}/gocode/src/%%{go_import_path}/{} \\;\n")
fmt.Fprintf(f, "find . -type f -name '*.go' -exec install -m 0644 {} %%{buildroot}%%{_datadir}/gocode/src/%%{go_import_path}/{} \\;\n")
case typeProgram:
fmt.Fprintf(f, "install -D -m 0755 %%{_name} %%{buildroot}%%{_bindir}/%%{_name}\n")
case typeLibraryProgram, typeProgramLibrary:
fmt.Fprintf(f, "# Install binary\n")
fmt.Fprintf(f, "install -D -m 0755 %%{_name} %%{buildroot}%%{_bindir}/%%{_name}\n")
fmt.Fprintf(f, "\n")
fmt.Fprintf(f, "# Install source code for library package\n")
fmt.Fprintf(f, "install -d %%{buildroot}%%{_datadir}/gocode/src/%%{go_import_path}\n")
fmt.Fprintf(f, "cp -pav *.go go.mod go.sum %%{buildroot}%%{_datadir}/gocode/src/%%{go_import_path}/ || :\n")
fmt.Fprintf(f, "find . -mindepth 1 -type d -not -path './.*' -exec install -d %%{buildroot}%%{_datadir}/gocode/src/%%{go_import_path}/{} \\;\n")
fmt.Fprintf(f, "find . -type f -name '*.go' -exec install -m 0644 {} %%{buildroot}%%{_datadir}/gocode/src/%%{go_import_path}/{} \\;\n")
}
fmt.Fprintf(f, "\n")
// %check
fmt.Fprintf(f, "%%check\n")
fmt.Fprintf(f, "export GOPATH=%%{_builddir}/go\n")
fmt.Fprintf(f, "go test -v ./...\n")
fmt.Fprintf(f, "\n")
// %files
writeRPMFilesSection(f, openRuyiSrc, openRuyiLib, openRuyiProgram, pkgType)
// %changelog
fmt.Fprintf(f, "%%changelog\n")
fmt.Fprintf(f, "%%{?autochangelog}\n")
return nil
}
// For library package
func writeRPMLibraryPackage(f *os.File, gopkg, openRuyiLib, longdesc string, deps []string) {
fmt.Fprintf(f, "BuildArch: noarch\n")
fmt.Fprintf(f, "\n")
// 库包的运行时依赖
if len(deps) > 0 {
for _, dep := range deps {
fmt.Fprintf(f, "Requires: %s\n", dep)
}
fmt.Fprintf(f, "\n")
}
}
// For library subpackage
func writeRPMLibrarySubpackage(f *os.File, gopkg, openRuyiLib, openRuyiSrc, longdesc string, deps []string) {
fmt.Fprintf(f, "\n")
fmt.Fprintf(f, "%%package -n %s\n", openRuyiLib)
fmt.Fprintf(f, "Summary: Development files of %s\n", filepath.Base(gopkg))
fmt.Fprintf(f, "BuildArch: noarch\n")
for _, dep := range deps {
fmt.Fprintf(f, "Requires: %s\n", dep)
}
fmt.Fprintf(f, "\n")
fmt.Fprintf(f, "%%description -n %s\n", openRuyiLib)
fmt.Fprintf(f, "%s\n", longdesc)
fmt.Fprintf(f, "\n")
fmt.Fprintf(f, "This package provides the Go source files of %s for development.\n")
}
// For program subpackage
func writeRPMProgramSubpackage(f *os.File, gopkg, openRuyiProgram, openRuyiSrc, description string) {
fmt.Fprintf(f, "\n")
fmt.Fprintf(f, "%%package -n %s\n", openRuyiProgram)
fmt.Fprintf(f, "Summary: Executable of %s\n", filepath.Base(gopkg))
fmt.Fprintf(f, "\n")
fmt.Fprintf(f, "%%description -n %s\n", openRuyiProgram)
fmt.Fprintf(f, "%s\n", description)
fmt.Fprintf(f, "\n")
fmt.Fprintf(f, "This package contains the %s executable.\n", filepath.Base(gopkg))
}
func writeRPMFilesSection(f *os.File, openRuyiSrc, openRuyiLib, openRuyiProgram string, pkgType packageType) {
switch pkgType {
case typeLibrary:
fmt.Fprintf(f, "%%files\n")
fmt.Fprintf(f, "%%license LICENSE* COPYING*\n")
fmt.Fprintf(f, "%%doc README* CHANGELOG* HISTORY*\n")
fmt.Fprintf(f, "%%{_datadir}/gocode/src/%%{go_import_path}\n")
fmt.Fprintf(f, "\n")
case typeProgram:
fmt.Fprintf(f, "%%files\n")
fmt.Fprintf(f, "%%license LICENSE* COPYING*\n")
fmt.Fprintf(f, "%%doc README* CHANGELOG* HISTORY*\n")
fmt.Fprintf(f, "%%{_bindir}/%%{_name}\n")
fmt.Fprintf(f, "\n")
case typeLibraryProgram:
// 库包文件(主包)
fmt.Fprintf(f, "%%files\n")
fmt.Fprintf(f, "%%license LICENSE* COPYING*\n")
fmt.Fprintf(f, "%%doc README* CHANGELOG* HISTORY*\n")
fmt.Fprintf(f, "%%{_datadir}/gocode/src/%%{go_import_path}\n")
fmt.Fprintf(f, "\n")
// 程序子包文件
fmt.Fprintf(f, "%%files -n %s\n", openRuyiProgram)
fmt.Fprintf(f, "%%license LICENSE* COPYING*\n")
fmt.Fprintf(f, "%%{_bindir}/%%{_name}\n")
fmt.Fprintf(f, "\n")
case typeProgramLibrary:
// 程序主包文件
fmt.Fprintf(f, "%%files\n")
fmt.Fprintf(f, "%%license LICENSE* COPYING*\n")
fmt.Fprintf(f, "%%doc README* CHANGELOG* HISTORY*\n")
fmt.Fprintf(f, "%%{_bindir}/%%{_name}\n")
fmt.Fprintf(f, "\n")
// 库子包文件
fmt.Fprintf(f, "%%files -n %s\n", openRuyiLib)
fmt.Fprintf(f, "%%license LICENSE* COPYING*\n")
fmt.Fprintf(f, "%%{_datadir}/gocode/src/%%{go_import_path}\n")
fmt.Fprintf(f, "\n")
}
}
func convertLongDescriptionForRPM(openRuyiSrc string) string {
// 移除 Debian 特有的格式
lines := strings.Split(openRuyiSrc, "\n")
var result []string
for _, line := range lines {
// 移除行首空格
line = strings.TrimPrefix(line, " ")
// 将 "." 单独行转换为空行
if line == "." {
line = ""
}
result = append(result, line)
}
return strings.Join(result, "\n")
}
// Sync with shortHostName() from pack.go
func getKnownHostsReverse() map[string]string {
return map[string]string{
"bazil": "bazil.org",
"bitbucket": "bitbucket.org",
"blitiri": "blitiri. com. ar",
"googlecloud": "cloud.google.com",
"googlecode": "code.google.com",
"codeberg": "codeberg.org",
"filippo": "filippo.io",
"fortio": "fortio.org",
"fyne": "fyne.io",
"sourcehut": "git.sr.ht",
"github": "github.com",
"gitlab": "gitlab.com",
"bugst": "go.bug.st",
"cypherpunks": "go.cypherpunks.ru",
"mongodb": "go.mongodb.org",
"opentelemetry": "go.opentelemetry.io",
"step": "go.step.sm",
"uber": "go.uber.org",
"go4": "go4.org",
"gocloud": "gocloud.dev",
"golang": "golang.org",
"google": "google.golang.org",
"gopkg": "gopkg.in",
"honnef": "honnef. co",
"howett": "howett.net",
"k8s": "k8s.io",
"modernc": "modernc.org",
"pault": "pault.ag",
"rsc": "rsc.io",
"debian": "salsa.debian.org",
"k8s-sigs": "sigs.k8s.io",
"sslmate": "software.sslmate.com",
"zgoat": "zgo.at",
}
}
func convertDependenciesToRPM(debDeps []string) []string {
knownHostsReverse := getKnownHostsReverse()
var rpmDeps []string
for _, dep := range debDeps {
// 转换 golang-xxx-dev 为 golang(xxx)
if strings.HasPrefix(dep, "golang-") && strings.HasSuffix(dep, "-dev") {
// golang-github-foo-bar-dev -> golang(github.com/foo/bar)
trimmed := strings.TrimPrefix(dep, "golang-")
trimmed = strings.TrimSuffix(trimmed, "-dev")
// 尝试还原 Go 包路径
parts := strings.Split(trimmed, "-")
if len(parts) >= 2 {
shortHost := parts[0]
// 使用 knownHosts 反向映射查找完整主机名
var fullHost string
if fqdn, ok := knownHostsReverse[shortHost]; ok {
fullHost = fqdn
} else {
// 未知主机,保持原样(可能是自定义域名)
fullHost = shortHost
log.Printf("WARNING: Unknown host shortname %q in dependency %q\n", shortHost, dep)
}
importPath := fullHost + "/" + strings.Join(parts[1:], "/")
rpmDeps = append(rpmDeps, fmt.Sprintf("golang(%s)", importPath))
} else {
rpmDeps = append(rpmDeps, fmt.Sprintf("golang(%s)", trimmed))
}
} else {
rpmDeps = append(rpmDeps, dep)
}
}
return rpmDeps
}