rpmmd looked at the CACHE_DIRECTORY environment variable to set a path for the dnf repository cache. Aside from being a smelly thing to do from a library, this breaks osbuild-pipeline and osbuild-dnf-json-tests, which don't run as systemd services and thus don't have CACHE_DIRECTORY set. Explicitly pass the cache directory to rpmmd. Keep using a path based on CACHE_DIRECTORY for osbuild-composer. Use the user's `.cache` directory for osbuild-pipeline and a temporary directory for the tests.
312 lines
7.6 KiB
Go
312 lines
7.6 KiB
Go
package rpmmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/gobwas/glob"
|
|
)
|
|
|
|
type RepoConfig struct {
|
|
Id string `json:"id"`
|
|
Name string `json:"name"`
|
|
BaseURL string `json:"baseurl,omitempty"`
|
|
Metalink string `json:"metalink,omitempty"`
|
|
MirrorList string `json:"mirrorlist,omitempty"`
|
|
GPGKey string `json:"gpgkey,omitempty"`
|
|
IgnoreSSL bool `json:"ignoressl"`
|
|
}
|
|
|
|
type PackageList []Package
|
|
|
|
type Package struct {
|
|
Name string
|
|
Summary string
|
|
Description string
|
|
URL string
|
|
Epoch uint
|
|
Version string
|
|
Release string
|
|
Arch string
|
|
BuildTime time.Time
|
|
License string
|
|
}
|
|
|
|
func (pkg Package) ToPackageBuild() PackageBuild {
|
|
return PackageBuild{
|
|
Arch: pkg.Arch,
|
|
BuildTime: pkg.BuildTime,
|
|
Epoch: pkg.Epoch,
|
|
Release: pkg.Release,
|
|
Source: PackageSource{
|
|
License: pkg.License,
|
|
Version: pkg.Version,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (pkg Package) ToPackageInfo() PackageInfo {
|
|
return PackageInfo{
|
|
Name: pkg.Name,
|
|
Summary: pkg.Summary,
|
|
Description: pkg.Description,
|
|
Homepage: pkg.URL,
|
|
Builds: []PackageBuild{pkg.ToPackageBuild()},
|
|
Dependencies: nil,
|
|
}
|
|
}
|
|
|
|
type PackageSpec struct {
|
|
Name string `json:"name"`
|
|
Epoch uint `json:"epoch"`
|
|
Version string `json:"version,omitempty"`
|
|
Release string `json:"release,omitempty"`
|
|
Arch string `json:"arch,omitempty"`
|
|
RemoteLocation string `json:"remote_location,omitempty"`
|
|
Checksum string `json:"checksum,omitempty"`
|
|
}
|
|
|
|
type PackageSource struct {
|
|
License string `json:"license"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
type PackageBuild struct {
|
|
Arch string `json:"arch"`
|
|
BuildTime time.Time `json:"build_time"`
|
|
Epoch uint `json:"epoch"`
|
|
Release string `json:"release"`
|
|
Source PackageSource `json:"source"`
|
|
}
|
|
|
|
type PackageInfo struct {
|
|
Name string `json:"name"`
|
|
Summary string `json:"summary"`
|
|
Description string `json:"description"`
|
|
Homepage string `json:"homepage"`
|
|
UpstreamVCS string `json:"upstream_vcs"`
|
|
Builds []PackageBuild `json:"builds"`
|
|
Dependencies []PackageSpec `json:"dependencies,omitempty"`
|
|
}
|
|
|
|
type RPMMD interface {
|
|
// FetchMetadata returns all metadata about the repositories we use in the code. Specifically it is a
|
|
// list of packages and dictionary of checksums of the repositories.
|
|
FetchMetadata(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error)
|
|
|
|
// Depsolve takes a list of required content (specs), explicitly unwanted content (excludeSpecs), list
|
|
// or repositories, and platform ID for modularity. It returns a list of all packages (with solved
|
|
// dependencies) that will be installed into the system.
|
|
Depsolve(specs, excludeSpecs []string, repos []RepoConfig, modulePlatformID string, clean bool) ([]PackageSpec, map[string]string, error)
|
|
}
|
|
|
|
type DNFError struct {
|
|
Kind string `json:"kind"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
func (err *DNFError) Error() string {
|
|
return fmt.Sprintf("DNF error occured: %s: %s", err.Kind, err.Reason)
|
|
}
|
|
|
|
type RepositoryError struct {
|
|
msg string
|
|
}
|
|
|
|
func (re *RepositoryError) Error() string {
|
|
return re.msg
|
|
}
|
|
|
|
func LoadRepositories(confPaths []string, distro string) (map[string][]RepoConfig, error) {
|
|
var f *os.File
|
|
var err error
|
|
path := "/repositories/" + distro + ".json"
|
|
|
|
for _, confPath := range confPaths {
|
|
f, err = os.Open(confPath + path)
|
|
if err == nil {
|
|
break
|
|
} else if !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, &RepositoryError{"LoadRepositories failed: none of the provided paths contain distro configuration"}
|
|
}
|
|
defer f.Close()
|
|
|
|
var repos map[string][]RepoConfig
|
|
|
|
err = json.NewDecoder(f).Decode(&repos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return repos, nil
|
|
}
|
|
|
|
func runDNF(command string, arguments interface{}, result interface{}) error {
|
|
var call = struct {
|
|
Command string `json:"command"`
|
|
Arguments interface{} `json:"arguments,omitempty"`
|
|
}{
|
|
command,
|
|
arguments,
|
|
}
|
|
|
|
cmd := exec.Command("python3", "dnf-json")
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Stderr = os.Stderr
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = json.NewEncoder(stdin).Encode(call)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stdin.Close()
|
|
|
|
output, err := ioutil.ReadAll(stdout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cmd.Wait()
|
|
|
|
const DnfErrorExitCode = 10
|
|
if runError, ok := err.(*exec.ExitError); ok && runError.ExitCode() == DnfErrorExitCode {
|
|
var dnfError DNFError
|
|
err = json.Unmarshal(output, &dnfError)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return &dnfError
|
|
}
|
|
|
|
err = json.Unmarshal(output, result)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type rpmmdImpl struct {
|
|
CacheDir string
|
|
}
|
|
|
|
func NewRPMMD(cacheDir string) RPMMD {
|
|
return &rpmmdImpl{
|
|
CacheDir: cacheDir,
|
|
}
|
|
}
|
|
|
|
func (r *rpmmdImpl) FetchMetadata(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error) {
|
|
var arguments = struct {
|
|
Repos []RepoConfig `json:"repos"`
|
|
CacheDir string `json:"cachedir"`
|
|
ModulePlatformID string `json:"module_platform_id"`
|
|
}{repos, r.CacheDir, modulePlatformID}
|
|
var reply struct {
|
|
Checksums map[string]string `json:"checksums"`
|
|
Packages PackageList `json:"packages"`
|
|
}
|
|
err := runDNF("dump", arguments, &reply)
|
|
sort.Slice(reply.Packages, func(i, j int) bool {
|
|
return reply.Packages[i].Name < reply.Packages[j].Name
|
|
})
|
|
return reply.Packages, reply.Checksums, err
|
|
}
|
|
|
|
func (r *rpmmdImpl) Depsolve(specs, excludeSpecs []string, repos []RepoConfig, modulePlatformID string, clean bool) ([]PackageSpec, map[string]string, error) {
|
|
var arguments = struct {
|
|
PackageSpecs []string `json:"package-specs"`
|
|
ExcludSpecs []string `json:"exclude-specs"`
|
|
Repos []RepoConfig `json:"repos"`
|
|
CacheDir string `json:"cachedir"`
|
|
ModulePlatformID string `json:"module_platform_id"`
|
|
Clean bool `json:"clean,omitempty"`
|
|
}{specs, excludeSpecs, repos, r.CacheDir, modulePlatformID, clean}
|
|
var reply struct {
|
|
Checksums map[string]string `json:"checksums"`
|
|
Dependencies []PackageSpec `json:"dependencies"`
|
|
}
|
|
err := runDNF("depsolve", arguments, &reply)
|
|
return reply.Dependencies, reply.Checksums, err
|
|
}
|
|
|
|
func (packages PackageList) Search(globPatterns ...string) (PackageList, error) {
|
|
var globs []glob.Glob
|
|
|
|
for _, globPattern := range globPatterns {
|
|
g, err := glob.Compile(globPattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
globs = append(globs, g)
|
|
}
|
|
|
|
var foundPackages PackageList
|
|
|
|
for _, pkg := range packages {
|
|
for _, g := range globs {
|
|
if g.Match(pkg.Name) {
|
|
foundPackages = append(foundPackages, pkg)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(packages, func(i, j int) bool {
|
|
return packages[i].Name < packages[j].Name
|
|
})
|
|
|
|
return foundPackages, nil
|
|
}
|
|
|
|
func (packages PackageList) ToPackageInfos() []PackageInfo {
|
|
resultsNames := make(map[string]int)
|
|
var results []PackageInfo
|
|
|
|
for _, pkg := range packages {
|
|
if index, ok := resultsNames[pkg.Name]; ok {
|
|
foundPkg := &results[index]
|
|
|
|
foundPkg.Builds = append(foundPkg.Builds, pkg.ToPackageBuild())
|
|
} else {
|
|
newIndex := len(results)
|
|
resultsNames[pkg.Name] = newIndex
|
|
|
|
packageInfo := pkg.ToPackageInfo()
|
|
|
|
results = append(results, packageInfo)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func (pkg *PackageInfo) FillDependencies(rpmmd RPMMD, repos []RepoConfig, modulePlatformID string) (err error) {
|
|
pkg.Dependencies, _, err = rpmmd.Depsolve([]string{pkg.Name}, nil, repos, modulePlatformID, false)
|
|
return
|
|
}
|