From bb24db1b54eba48e82c34597c799aa96561101b6 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Wed, 28 Sep 2022 18:44:29 +0200 Subject: [PATCH] feat: add inital support for source RPMs --- internal/artifact/artifact.go | 8 ++ internal/pipe/srpm/spec.tmpl | 65 ++++++++++ internal/pipe/srpm/srpm.go | 220 ++++++++++++++++++++++++++++++++ internal/pipe/srpm/srpm_test.go | 82 ++++++++++++ internal/pipeline/pipeline.go | 2 + pkg/config/config.go | 25 ++++ pkg/defaults/defaults.go | 2 + 7 files changed, 404 insertions(+) create mode 100644 internal/pipe/srpm/spec.tmpl create mode 100644 internal/pipe/srpm/srpm.go create mode 100644 internal/pipe/srpm/srpm_test.go diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go index 06434e02c4c1..d7bb689b8765 100644 --- a/internal/artifact/artifact.go +++ b/internal/artifact/artifact.go @@ -66,6 +66,10 @@ const ( ScoopManifest // SBOM is a Software Bill of Materials file. SBOM + // SourceRPM is a source RPM. + SourceRPM + // RPMSpec is an RPM .spec file. + RPMSpec ) func (t Type) String() string { @@ -104,6 +108,10 @@ func (t Type) String() string { return "PKGBUILD" case SrcInfo: return "SRCINFO" + case SourceRPM: + return "Source RPM" + case RPMSpec: + return "RPM Spec" default: return "unknown" } diff --git a/internal/pipe/srpm/spec.tmpl b/internal/pipe/srpm/spec.tmpl new file mode 100644 index 000000000000..b97e5a277fbf --- /dev/null +++ b/internal/pipe/srpm/spec.tmpl @@ -0,0 +1,65 @@ +# Generated by goreleaser +%bcond_without check + +%global goipath {{ .ImportPath }} +%global commit {{ .FullCommit }} + +%%gometa -f + +%global common_description %{expand: +{{ .Description }}} + +{{ if .LicenseFileName }} +%global golicenses {{ .LicenseFileName }} +{{ end }} +{{ if .Docs }} +%global godocs {{ range .Docs }} {{ . }}{{ end }} +{{ end }} + +Name: %{goname} +Version: {{ .Version }} +Release: %autorelease -p +Summary: {{ .Summary }} + +License: {{ .License }} +URL: {{ .URL }} +Source: {{ .Source }} + +%description %{common_description} + +%gopkg + +%prep +%goprep + +%generate_buildrequires +%go_generate_buildrequires + +%build +{{ range $binary, $importPath := .Bins }} +%gobuild -o %{gobuilddir}/bin/{{ $binary }} {{ $importPath }} +{{ end }} + +%install +%gopkginstall +install -m 0755 -vd %{buildroot}%{_bindir} +install -m 0755 -vp %{gobuilddir}/bin/* %{buildroot}%{_bindir}/ + +%if %{with check} +%check +%gocheck +%endif + +%files +{{ range .Docs }} +%doc {{ . }} +{{ end }} +{{ if .LicenseFileName }} +%license {{ .LicenseFileName }} +{{ end }} +%{_bindir}/* + +%gopkgfiles + +%changelog +%autochangelog diff --git a/internal/pipe/srpm/srpm.go b/internal/pipe/srpm/srpm.go new file mode 100644 index 000000000000..333f513fb5f0 --- /dev/null +++ b/internal/pipe/srpm/srpm.go @@ -0,0 +1,220 @@ +// Package srpm implements the Pipe interface building source RPMs. +package srpm + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/caarlos0/log" + "github.com/goreleaser/goreleaser/internal/artifact" + "github.com/goreleaser/goreleaser/internal/tmpl" + "github.com/goreleaser/goreleaser/pkg/context" + "github.com/goreleaser/nfpm/v2" + "github.com/goreleaser/nfpm/v2/files" + + _ "github.com/goreleaser/nfpm/v2/rpm" // blank import to register the format +) + +var ( + defaultFileNameTemplate = "{{ .PackageName }}-{{ .Version }}.src.rpm" + defaultSpecFileNameTemplate = "{{ .PackageName }}.spec" +) + +//go:embed spec.tmpl +var defaultSpecTemplate string + +// Pipe for source RPMs. +type Pipe struct{} + +func (Pipe) String() string { return "source RPMs" } +func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.SRPM.Enabled } + +// Default sets the pipe defaults. +func (Pipe) Default(ctx *context.Context) error { + srpm := &ctx.Config.SRPM + if srpm.ID == "" { + srpm.ID = "default" + } + if srpm.PackageName == "" { + srpm.PackageName = ctx.Config.ProjectName + } + if srpm.FileNameTemplate == "" { + srpm.FileNameTemplate = defaultFileNameTemplate + } + if srpm.SpecFileNameTemplate == "" { + srpm.SpecFileNameTemplate = defaultSpecFileNameTemplate + } + if srpm.SpecTemplate == "" { + srpm.SpecTemplate = defaultSpecTemplate + } + if srpm.Bins == nil { + srpm.Bins = map[string]string{ + ctx.Config.ProjectName: "%{goipath}", + } + } + return nil +} + +// Run the pipe. +func (Pipe) Run(ctx *context.Context) error { + sourceArchives := ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableSourceArchive)).List() + if len(sourceArchives) == 0 { + return fmt.Errorf("no source archives found") + } else if len(sourceArchives) > 1 { + return fmt.Errorf("multiple source archives found") + } + + srpm := ctx.Config.SRPM + sourceArchive := sourceArchives[0] + + t := tmpl.New(ctx). + WithExtraFields(tmpl.Fields{ + "PackageName": srpm.PackageName, + "ImportPath": srpm.ImportPath, + "License": srpm.License, + "LicenseFileName": srpm.LicenseFileName, + "URL": srpm.URL, + "Summary": srpm.Summary, + "Description": srpm.Description, + "Source": sourceArchive.Name, + "Bins": srpm.Bins, + "Docs": srpm.Docs, + }) + + // Generate the spec file. + specFileName, err := t.Apply(srpm.SpecFileNameTemplate) + if err != nil { + return err + } + specContents, err := t.Apply(srpm.SpecTemplate) + if err != nil { + return err + } + specPath := filepath.Join(ctx.Config.Dist, specFileName) + if err := os.WriteFile(specPath, []byte(specContents), 0o666); err != nil { + return err + } + specFileArtifact := &artifact.Artifact{ + Type: artifact.RPMSpec, + Name: specFileName, + Path: specPath, + } + + // Default file info. + owner := "mockbuild" + group := "mock" + mtime := ctx.Git.CommitDate + + contents := files.Contents{} + + // Add the source archive. + contents = append(contents, &files.Content{ + Source: sourceArchive.Path, + Destination: sourceArchive.Name, + // FIXME Type: + Packager: srpm.Packager, + FileInfo: &files.ContentFileInfo{ + Owner: owner, + Group: group, + Mode: 0o664, // Source archives are group-writeable by default. + MTime: mtime, + // FIXME Size: + }, + }) + + // Add extra contents. + contents = append(contents, srpm.Contents...) + + // Add the spec file. + contents = append(contents, &files.Content{ + Source: specFileArtifact.Path, + Destination: specFileArtifact.Name, + // FIXME Type: + Packager: srpm.Packager, + FileInfo: &files.ContentFileInfo{ + Owner: owner, + Group: group, + Mode: 0o660, // Spec files are private by default. + MTime: mtime, + Size: int64(len(specContents)), + }, + }) + + keyFile, err := t.Apply(srpm.Signature.KeyFile) + if err != nil { + return err + } + + // Create the source RPM package. + info := &nfpm.Info{ + Name: srpm.PackageName, + Epoch: srpm.Epoch, + Version: ctx.Version, + Section: srpm.Section, + Maintainer: srpm.Maintainer, + Description: srpm.Description, + Vendor: srpm.Vendor, + Homepage: srpm.URL, + License: srpm.License, + Overridables: nfpm.Overridables{ + Contents: contents, + RPM: nfpm.RPM{ + Group: srpm.Group, + Summary: srpm.Summary, + Compression: srpm.Compression, + Packager: srpm.Packager, + Signature: nfpm.RPMSignature{ + PackageSignature: nfpm.PackageSignature{ + KeyFile: keyFile, + KeyPassphrase: ctx.Env[fmt.Sprintf("SRPM_%s_PASSPHRASE", srpm.ID)], + // TODO: KeyID + }, + }, + }, + }, + } + + if ctx.SkipSign { + info.RPM.Signature = nfpm.RPMSignature{} + } + + packager, err := nfpm.Get("rpm") + if err != nil { + return err + } + info = nfpm.WithDefaults(info) + + // Write the source RPM. + srpmFileName, err := t.Apply(srpm.FileNameTemplate) + if err != nil { + return err + } + if !strings.HasSuffix(srpmFileName, ".src.rpm") { + srpmFileName += ".src.rpm" + } + srpmPath := filepath.Join(ctx.Config.Dist, srpmFileName) + log.WithField("file", srpmPath).Info("creating") + srpmFile, err := os.Create(srpmPath) + if err != nil { + return err + } + defer srpmFile.Close() + if err := packager.Package(info, srpmFile); err != nil { + return fmt.Errorf("nfpm failed: %w", err) + } + if err := srpmFile.Close(); err != nil { + return fmt.Errorf("could not close package file: %w", err) + } + srpmArtifact := &artifact.Artifact{ + Type: artifact.SourceRPM, + Name: srpmFileName, + Path: srpmPath, + } + + ctx.Artifacts.Add(specFileArtifact) + ctx.Artifacts.Add(srpmArtifact) + return nil +} diff --git a/internal/pipe/srpm/srpm_test.go b/internal/pipe/srpm/srpm_test.go new file mode 100644 index 000000000000..3e5041d1c1ad --- /dev/null +++ b/internal/pipe/srpm/srpm_test.go @@ -0,0 +1,82 @@ +package srpm + +import ( + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/goreleaser/goreleaser/internal/artifact" + "github.com/goreleaser/goreleaser/pkg/config" + "github.com/goreleaser/goreleaser/pkg/context" + "github.com/stretchr/testify/require" +) + +func TestRunPipe(t *testing.T) { + // Setup a context with a source archive. + folder := t.TempDir() + dist := filepath.Join(folder, "dist") + require.NoError(t, os.Mkdir(dist, 0o755)) + sourceArchivePath := filepath.Join(dist, "example-1.0.0.tar.gz") + f, err := os.Create(sourceArchivePath) + require.NoError(t, err) + require.NoError(t, f.Close()) + ctx := context.New(config.Project{ + ProjectName: "example", + Dist: dist, + SRPM: config.SRPM{ + NFPMRPM: config.NFPMRPM{ + Summary: "Example summary", + }, + Enabled: true, + ImportPath: "github.com/example/example", + License: "MIT", + LicenseFileName: "LICENSE", + Packager: "Example packager", + Vendor: "Example vendor", + URL: "https://example.com", + Description: "Example description", + Docs: []string{ + "README.md", + }, + }, + }) + ctx.Version = "1.0.0" + ctx.Git = context.GitInfo{ + FullCommit: "e070258c90772fbcf1cb94c2b937ff25a011b5c8", + } + ctx.Artifacts.Add(&artifact.Artifact{ + Name: "example-1.0.0.tar.gz", + Path: sourceArchivePath, + Type: artifact.UploadableSourceArchive, + }) + + // Run the source RPM pipe. + var pipe Pipe + require.NoError(t, pipe.Default(ctx)) + require.NoError(t, pipe.Run(ctx)) + + // Check the source RPM artifact. + sourceRPMs := ctx.Artifacts.Filter(artifact.ByType(artifact.SourceRPM)).List() + require.Len(t, sourceRPMs, 1) + sourceRPM := sourceRPMs[0] + require.Equal(t, "example-1.0.0.src.rpm", sourceRPM.Name) + require.Equal(t, filepath.Join(dist, "example-1.0.0.src.rpm"), sourceRPM.Path) + // FIXME check source RPM contents using https://github.com/sassoftware/go-rpmutils? + // FIXME check source RPM contents using https://github.com/cavaliergopher/rpm? + + // Check the .spec artifact. + rpmSpecs := ctx.Artifacts.Filter(artifact.ByType(artifact.RPMSpec)).List() + require.Len(t, rpmSpecs, 1) + rpmSpecContents, err := os.ReadFile(rpmSpecs[0].Path) + require.NoError(t, err) + require.True(t, regexp.MustCompile(`(?m)^%global\s+goipath\s+github.com/example/example$`).Match(rpmSpecContents)) + require.True(t, regexp.MustCompile(`(?m)^%global\s+commit\s+e070258c90772fbcf1cb94c2b937ff25a011b5c8$`).Match(rpmSpecContents)) + require.True(t, regexp.MustCompile(`(?m)^%global\s+golicenses\s+LICENSE$`).Match(rpmSpecContents)) + require.True(t, regexp.MustCompile(`(?m)^%global\s+godocs\s+README\.md$`).Match(rpmSpecContents)) + require.True(t, regexp.MustCompile(`(?m)^Version:\s+1\.0\.0$`).Match(rpmSpecContents)) + require.True(t, regexp.MustCompile(`(?m)^Summary:\s+Example summary$`).Match(rpmSpecContents)) + require.True(t, regexp.MustCompile(`(?m)^%doc\s+README\.md$`).Match(rpmSpecContents)) + require.True(t, regexp.MustCompile(`(?m)^%license\s+LICENSE$`).Match(rpmSpecContents)) + // FIXME add tests for all remaining configurable fields +} diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 8373ea6ec07f..2dcb0051f197 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -31,6 +31,7 @@ import ( "github.com/goreleaser/goreleaser/internal/pipe/snapcraft" "github.com/goreleaser/goreleaser/internal/pipe/snapshot" "github.com/goreleaser/goreleaser/internal/pipe/sourcearchive" + "github.com/goreleaser/goreleaser/internal/pipe/srpm" "github.com/goreleaser/goreleaser/internal/pipe/universalbinary" "github.com/goreleaser/goreleaser/pkg/context" ) @@ -73,6 +74,7 @@ var Pipeline = append( archive.Pipe{}, // archive in tar.gz, zip or binary (which does no archiving at all) sourcearchive.Pipe{}, // archive the source code using git-archive nfpm.Pipe{}, // archive via fpm (deb, rpm) using "native" go impl + srpm.Pipe{}, // create source RPMs snapcraft.Pipe{}, // archive via snapcraft (snap) sbom.Pipe{}, // create SBOMs of artifacts checksums.Pipe{}, // checksums of the files diff --git a/pkg/config/config.go b/pkg/config/config.go index ae2ebe9258a2..2f94b89f8997 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -655,6 +655,30 @@ type NFPMOverridables struct { APK NFPMAPK `yaml:"apk,omitempty" json:"apk,omitempty"` } +// SRPM is used to specify source RPMs. +type SRPM struct { + NFPMRPM + Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` + ID string `yaml:"id,omitempty" json:"id,omitempty"` + PackageName string `yaml:"package_name,omitempty" json:"package_name,omitempty"` + Epoch string `yaml:"epoch,omitempty" json:"epoch,omitempty"` + ImportPath string `yaml:"import_path,omitempty" json:"import_path,omitempty"` + Section string `yaml:"section,omitempty" json:"section,omitempty"` + Maintainer string `yaml:"maintainer,omitempty" json:"maintainer,omitempty"` + FileNameTemplate string `yaml:"file_name_template,omitempty" json:"file_name_template,omitempty"` + SpecFileNameTemplate string `yaml:"spec_file_name_template,omitempty" json:"spec_file_name_template,omitempty"` + SpecTemplate string `yaml:"spec_template,omitempty" json:"spec_template,omitempty"` + License string `yaml:"license,omitempty" json:"license,omitempty"` + LicenseFileName string `yaml:"license_file_name,omitempty" json:"license_file_name,omitempty"` + Vendor string `yaml:"vendor,omitempty" json:"vendor,omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` + Packager string `yaml:"packager,omitempty" json:"packager,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Bins map[string]string `yaml:"bins,omitempty" json:"bins,omitempty"` + Docs []string `yaml:"docs,omitempty" json:"docs,omitempty"` + Contents files.Contents `yaml:"contents,omitempty" json:"contents,omitempty"` +} + // SBOM config. type SBOM struct { ID string `yaml:"id,omitempty" json:"id,omitempty"` @@ -913,6 +937,7 @@ type Project struct { EnvFiles EnvFiles `yaml:"env_files,omitempty" json:"env_files,omitempty"` Before Before `yaml:"before,omitempty" json:"before,omitempty"` Source Source `yaml:"source,omitempty" json:"source,omitempty"` + SRPM SRPM `yaml:"srpm,omitempty" json:"srpm,omitempty"` GoMod GoMod `yaml:"gomod,omitempty" json:"gomod,omitempty"` Announce Announce `yaml:"announce,omitempty" json:"announce,omitempty"` SBOMs []SBOM `yaml:"sboms,omitempty" json:"sboms,omitempty"` diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index bdb625e12ae7..a1d31396d80e 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -31,6 +31,7 @@ import ( "github.com/goreleaser/goreleaser/internal/pipe/snapcraft" "github.com/goreleaser/goreleaser/internal/pipe/snapshot" "github.com/goreleaser/goreleaser/internal/pipe/sourcearchive" + "github.com/goreleaser/goreleaser/internal/pipe/srpm" "github.com/goreleaser/goreleaser/internal/pipe/teams" "github.com/goreleaser/goreleaser/internal/pipe/telegram" "github.com/goreleaser/goreleaser/internal/pipe/twitter" @@ -60,6 +61,7 @@ var Defaulters = []Defaulter{ sourcearchive.Pipe{}, archive.Pipe{}, nfpm.Pipe{}, + srpm.Pipe{}, snapcraft.Pipe{}, checksums.Pipe{}, sign.Pipe{},