forked from misaka00251/go2spec
Init project
This commit is contained in:
169
check_version.go
Normal file
169
check_version.go
Normal 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
99
description.go
Normal 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 Debian’s 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, we’d use https://github.com/github/markup
|
||||
// itself to render to HTML, then convert HTML to plaintext. That sounds
|
||||
// fairly involved, but it’d 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
96
description.json
Normal 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
39
go.mod
Normal 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
81
go.sum
Normal 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
61
main.go
Normal 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
130
metadata.go
Normal 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
742
pack.go
Normal 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
58
progress.go
Normal 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
340
spec.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user