forked from misaka00251/go2spec
368 lines
11 KiB
Go
368 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type specAssetFiles struct {
|
|
licenseFiles []string
|
|
readmeFile string
|
|
}
|
|
|
|
func cleanSpecAssetPath(path string) string {
|
|
path = filepath.ToSlash(strings.TrimSpace(path))
|
|
path = strings.TrimLeft(path, "/")
|
|
if path == "" || strings.HasPrefix(path, "../") || strings.Contains(path, "/../") || path == ".." {
|
|
return ""
|
|
}
|
|
return path
|
|
}
|
|
|
|
func pkgsiteLicenseFilePaths(licenses []pkgsiteLicense) []string {
|
|
topLevel := make([]pkgsiteLicense, 0, len(licenses))
|
|
for _, license := range licenses {
|
|
if !strings.Contains(strings.Trim(license.FilePath, "/"), "/") {
|
|
topLevel = append(topLevel, license)
|
|
}
|
|
}
|
|
if len(topLevel) > 0 {
|
|
licenses = topLevel
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
paths := make([]string, 0, len(licenses))
|
|
for _, license := range licenses {
|
|
path := cleanSpecAssetPath(license.FilePath)
|
|
if path == "" || seen[path] {
|
|
continue
|
|
}
|
|
seen[path] = true
|
|
paths = append(paths, path)
|
|
}
|
|
sort.Strings(paths)
|
|
return paths
|
|
}
|
|
|
|
func getSpecAssetFilesForGopkg(gopkg string) (specAssetFiles, error) {
|
|
info, err := getPkgsiteInfo(context.TODO(), gopkg)
|
|
if err != nil {
|
|
return specAssetFiles{}, err
|
|
}
|
|
|
|
licenses := info.Module.Licenses
|
|
if len(licenses) == 0 {
|
|
licenses = info.Package.Licenses
|
|
}
|
|
|
|
assets := specAssetFiles{
|
|
licenseFiles: pkgsiteLicenseFilePaths(licenses),
|
|
}
|
|
if info.Module.Readme != nil {
|
|
assets.readmeFile = cleanSpecAssetPath(info.Module.Readme.Filepath)
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
func writeSpec(dir, gopkg, openRuyiSrc, openRuyiLib, openRuyiProgram, version string,
|
|
pkgType packageType, 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"
|
|
}
|
|
assetFiles, err := getSpecAssetFilesForGopkg(gopkg)
|
|
if err != nil {
|
|
log.Printf("Could not determine license/doc file paths for %q: %v\n", gopkg, err)
|
|
}
|
|
upstreamName := filepath.Base(gopkg)
|
|
if regexp.MustCompile(`^v\d+$`).MatchString(upstreamName) {
|
|
upstreamName = filepath.Base(filepath.Dir(gopkg))
|
|
}
|
|
repoURL, err := getRepoURLForGopkg(gopkg)
|
|
if err != nil {
|
|
log.Printf("Could not determine repository URL for %q: %v\n", gopkg, err)
|
|
repoURL = "TODO"
|
|
}
|
|
|
|
// Write the spec file content
|
|
|
|
// SPDX header
|
|
fmt.Fprintf(f, "# SPDX-FileCopyrightText: (C) 2026 Institute of Software, Chinese Academy of Sciences (ISCAS)\n")
|
|
fmt.Fprintf(f, "# SPDX-FileCopyrightText: (C) 2026 openRuyi Project Contributors\n")
|
|
fmt.Fprintf(f, "# SPDX-FileContributor: \n")
|
|
fmt.Fprintf(f, "#\n")
|
|
fmt.Fprintf(f, "# SPDX-License-Identifier: MulanPSL-2.0\n")
|
|
fmt.Fprintf(f, "\n")
|
|
|
|
// Macros
|
|
fmt.Fprintf(f, "%%define _name %s\n", upstreamName)
|
|
fmt.Fprintf(f, "%%define go_import_path %s\n", gopkg)
|
|
|
|
// If pkgVersionFromGit embedded a commit_id define in u.version, extract and write it here
|
|
commitRe := regexp.MustCompile(`%define\s+commit_id\s+([0-9a-fA-F]+)`)
|
|
if m := commitRe.FindStringSubmatch(u.version); m != nil {
|
|
fmt.Fprintf(f, "%%define commit_id %s\n", m[1])
|
|
// Remove the embedded %define line from version so it doesn't get written to Version: field
|
|
version = commitRe.ReplaceAllString(version, "")
|
|
version = strings.TrimSpace(version)
|
|
}
|
|
fmt.Fprintf(f, "\n")
|
|
|
|
// Header
|
|
fmt.Fprintf(f, "Name: %s\n", openRuyiSrc)
|
|
|
|
// Program packages may need manual naming cleanup; keep the library-form
|
|
// name visible as a second Name line until that workflow is redesigned.
|
|
switch pkgType {
|
|
case typeProgram:
|
|
fmt.Fprintf(f, "Name: %s\n", openRuyiLib)
|
|
}
|
|
|
|
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: %s\n", repoURL)
|
|
if u.sha256 != "" {
|
|
fmt.Fprintf(f, "#!RemoteAsset: sha256:%s\n", u.sha256)
|
|
} else {
|
|
fmt.Fprintf(f, "#!RemoteAsset\n")
|
|
}
|
|
sourceModulePath := u.modulePath
|
|
if sourceModulePath == "" {
|
|
sourceModulePath = gopkg
|
|
}
|
|
sourceURL, err := u.sourceURLForSpec(sourceModulePath)
|
|
if err != nil {
|
|
log.Printf("Could not determine remote source URL for %q: %v\n", gopkg, err)
|
|
sourceURL = "TODO"
|
|
}
|
|
fmt.Fprintf(f, "Source0: %s\n", sourceURL)
|
|
|
|
switch pkgType {
|
|
case typeLibrary:
|
|
fmt.Fprintf(f, "BuildArch: noarch\n")
|
|
fmt.Fprintf(f, "BuildSystem: golangmodules\n")
|
|
fmt.Fprintf(f, "\n")
|
|
case typeProgram, typeLibraryProgram, typeProgramLibrary:
|
|
fmt.Fprintf(f, "\n")
|
|
}
|
|
|
|
fmt.Fprintf(f, "BuildRequires: go\n")
|
|
fmt.Fprintf(f, "BuildRequires: go-rpm-macros\n")
|
|
// And other BuildRequires from dependencies
|
|
rpmBuildDeps := convertDependenciesToRPM(u.repoDeps)
|
|
sort.Strings(rpmBuildDeps)
|
|
for _, dep := range rpmBuildDeps {
|
|
fmt.Fprintf(f, "BuildRequires: %s\n", dep)
|
|
}
|
|
rpmRuntimeDeps := convertDependenciesToRPM(u.repoRunDeps)
|
|
sort.Strings(rpmRuntimeDeps)
|
|
|
|
// For different package types, write different sections
|
|
switch pkgType {
|
|
case typeLibrary:
|
|
writeRPMLibraryPackage(f, gopkg, openRuyiLib, longdescription, rpmRuntimeDeps)
|
|
case typeProgram:
|
|
log.Printf("Nothing to do for program package.\n")
|
|
// TODO: what can this be used for? ExclusiveArch %%{go_arches}?
|
|
// writeRPMProgramPackage(f, gopkg, openRuyiProgram, longdescription)
|
|
case typeLibraryProgram:
|
|
writeRPMLibraryPackage(f, gopkg, openRuyiLib, longdescription, rpmRuntimeDeps)
|
|
writeRPMProgramSubpackage(f, gopkg, openRuyiProgram, openRuyiSrc, description)
|
|
case typeProgramLibrary:
|
|
//writeRPMProgramPackage(f, gopkg, openRuyiProgram, longdescription)
|
|
writeRPMLibrarySubpackage(f, gopkg, openRuyiLib, openRuyiSrc, longdescription, rpmRuntimeDeps)
|
|
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")
|
|
|
|
// %files
|
|
writeRPMFilesSection(f, openRuyiSrc, openRuyiLib, openRuyiProgram, pkgType, assetFiles)
|
|
|
|
// %changelog
|
|
fmt.Fprintf(f, "%%changelog\n")
|
|
fmt.Fprintf(f, "%%{?autochangelog}\n\n")
|
|
|
|
return nil
|
|
}
|
|
|
|
// For library package
|
|
func writeRPMLibraryPackage(f *os.File, gopkg, openRuyiLib, longdesc string, deps []string) {
|
|
fmt.Fprintf(f, "\n")
|
|
fmt.Fprintf(f, "Provides: go(%s) = %%{version}\n", gopkg)
|
|
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, "Provides: go(%s) = %%{version}\n", gopkg)
|
|
fmt.Fprintf(f, "BuildArch: noarch\n")
|
|
if len(deps) > 0 {
|
|
for _, dep := range deps {
|
|
fmt.Fprintf(f, "Requires: %s\n", dep)
|
|
}
|
|
} else {
|
|
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", gopkg)
|
|
}
|
|
|
|
// 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 writeLicenseAndDocFiles(f *os.File, assetFiles specAssetFiles, includeDoc bool) {
|
|
if includeDoc && assetFiles.readmeFile != "" {
|
|
fmt.Fprintf(f, "%%doc %s\n", assetFiles.readmeFile)
|
|
}
|
|
licenseFiles := append([]string(nil), assetFiles.licenseFiles...)
|
|
sort.Strings(licenseFiles)
|
|
for _, licenseFile := range licenseFiles {
|
|
fmt.Fprintf(f, "%%license %s\n", licenseFile)
|
|
}
|
|
}
|
|
|
|
func writeRPMFilesSection(f *os.File, openRuyiSrc, openRuyiLib, openRuyiProgram string, pkgType packageType, assetFiles specAssetFiles) {
|
|
switch pkgType {
|
|
case typeLibrary:
|
|
fmt.Fprintf(f, "%%files\n")
|
|
writeLicenseAndDocFiles(f, assetFiles, true)
|
|
fmt.Fprintf(f, "%%{go_sys_gopath}/%%{go_import_path}\n")
|
|
fmt.Fprintf(f, "\n")
|
|
|
|
case typeProgram:
|
|
fmt.Fprintf(f, "%%files\n")
|
|
writeLicenseAndDocFiles(f, assetFiles, true)
|
|
fmt.Fprintf(f, "%%{_bindir}/%%{_name}\n")
|
|
fmt.Fprintf(f, "\n")
|
|
|
|
case typeLibraryProgram:
|
|
// 库包文件(主包)
|
|
fmt.Fprintf(f, "%%files\n")
|
|
writeLicenseAndDocFiles(f, assetFiles, true)
|
|
fmt.Fprintf(f, "%%{go_sys_gopath}/%%{go_import_path}\n")
|
|
fmt.Fprintf(f, "\n")
|
|
// 程序子包文件
|
|
fmt.Fprintf(f, "%%files -n %s\n", openRuyiProgram)
|
|
writeLicenseAndDocFiles(f, assetFiles, false)
|
|
fmt.Fprintf(f, "%%{_bindir}/%%{_name}\n")
|
|
fmt.Fprintf(f, "\n")
|
|
|
|
case typeProgramLibrary:
|
|
// 程序主包文件
|
|
fmt.Fprintf(f, "%%files\n")
|
|
writeLicenseAndDocFiles(f, assetFiles, true)
|
|
fmt.Fprintf(f, "%%{_bindir}/%%{_name}\n")
|
|
fmt.Fprintf(f, "\n")
|
|
// 库子包文件
|
|
fmt.Fprintf(f, "%%files -n %s\n", openRuyiLib)
|
|
writeLicenseAndDocFiles(f, assetFiles, false)
|
|
fmt.Fprintf(f, "%%{go_sys_gopath}/%%{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")
|
|
}
|
|
|
|
func convertDependenciesToRPM(goPkgs []string) []string {
|
|
// 使用 map 来去重和提取顶级包路径
|
|
topLevelPkgs := make(map[string]bool)
|
|
|
|
for _, goPkg := range goPkgs {
|
|
// 提取顶级包:github.com/user/repo/subpkg -> github.com/user/repo
|
|
parts := strings.Split(goPkg, "/")
|
|
var topLevel string
|
|
|
|
if len(parts) >= 3 {
|
|
// 对于大多数情况(github.com/user/repo/...),只取前三部分
|
|
topLevel = strings.Join(parts[:3], "/")
|
|
|
|
// 例外情况:如果第四部分以 'v' 开头(版本号),则保留第四部分
|
|
// 例如:github.com/minio/madmin-go/v3 -> github.com/minio/madmin-go/v3
|
|
if len(parts) >= 4 && strings.HasPrefix(parts[3], "v") {
|
|
topLevel = strings.Join(parts[:4], "/")
|
|
}
|
|
} else if len(parts) > 0 {
|
|
// 如果路径部分少于 3 个,保留整个路径
|
|
topLevel = goPkg
|
|
}
|
|
|
|
topLevelPkgs[topLevel] = true
|
|
}
|
|
|
|
// 转换为 RPM 格式并排序
|
|
var rpmDeps []string
|
|
for pkg := range topLevelPkgs {
|
|
rpmDeps = append(rpmDeps, fmt.Sprintf("go(%s)", pkg))
|
|
}
|
|
sort.Strings(rpmDeps)
|
|
|
|
return rpmDeps
|
|
}
|