mirror of
https://github.com/clearlinux/mixer-tools.git
synced 2026-09-04 12:41:37 +00:00
When building the upstream URL, if the b.UpstreamURL was set do a url with a subpath included, the subpath was being discarded by URL.ResolveReference(). In order to preserve the subpath in the base, the provided subpath must be manually added to the base URL object. Signed-off-by: Rodrigo Chiossi <rodrigo.chiossi@intel.com>
2415 lines
69 KiB
Go
2415 lines
69 KiB
Go
// Copyright © 2017 Intel Corporation
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package builder
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"text/tabwriter"
|
|
textTemplate "text/template" // "template" conflicts with crypto/x509
|
|
|
|
"github.com/clearlinux/mixer-tools/config"
|
|
"github.com/clearlinux/mixer-tools/helpers"
|
|
"github.com/clearlinux/mixer-tools/swupd"
|
|
"github.com/go-ini/ini"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Version of Mixer. This is provided by ldflags in Makefile during compilation
|
|
var Version = ""
|
|
|
|
// Native controls whether mixer runs the command on the native machine or in a
|
|
// container.
|
|
var Native = true
|
|
|
|
// Offline controls whether mixer attempts to automatically cache upstream
|
|
// bundles. In offline mode, all necessary bundles must exist in local-bundles.
|
|
var Offline = false
|
|
|
|
// A Builder contains all configurable fields required to perform a full mix
|
|
// operation, and is used to encapsulate life time data.
|
|
type Builder struct {
|
|
Config config.MixConfig
|
|
State config.MixState
|
|
|
|
MixVer string
|
|
MixVerFile string
|
|
MixBundlesFile string
|
|
LocalPackagesFile string
|
|
UpstreamURL string
|
|
UpstreamURLFile string
|
|
UpstreamVer string
|
|
UpstreamVerFile string
|
|
|
|
Signing int
|
|
Bump int
|
|
|
|
NumFullfileWorkers int
|
|
NumDeltaWorkers int
|
|
NumBundleWorkers int
|
|
|
|
// Parsed versions.
|
|
MixVerUint32 uint32
|
|
UpstreamVerUint32 uint32
|
|
}
|
|
|
|
// UpdateParameters contains the configuration parameters for building an update
|
|
type UpdateParameters struct {
|
|
// Minimum version used to generate delta packs
|
|
MinVersion int
|
|
// Format version used in this update
|
|
Format string
|
|
// Update latest format version and image version files to current mix
|
|
Publish bool
|
|
// Skip signing Manifest.MoM
|
|
SkipSigning bool
|
|
// Skip fullfiles generation
|
|
SkipFullfiles bool
|
|
// Skip zero packs generation
|
|
SkipPacks bool
|
|
}
|
|
|
|
var localPackages = make(map[string]bool)
|
|
var upstreamPackages = make(map[string]bool)
|
|
|
|
// New will return a new instance of Builder with some predetermined sane
|
|
// default values.
|
|
func New() *Builder {
|
|
return &Builder{
|
|
UpstreamURLFile: "upstreamurl",
|
|
UpstreamVerFile: "upstreamversion",
|
|
MixBundlesFile: "mixbundles",
|
|
LocalPackagesFile: "local-packages",
|
|
MixVerFile: "mixversion",
|
|
|
|
Signing: 1,
|
|
Bump: 0,
|
|
}
|
|
}
|
|
|
|
// NewFromConfig creates a new Builder with the given Configuration.
|
|
func NewFromConfig(conf string) (*Builder, error) {
|
|
b := New()
|
|
if err := b.Config.LoadDefaults(false); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := b.State.Load(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := b.Config.LoadConfig(conf); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := b.ReadVersions(); err != nil {
|
|
return nil, err
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// initDirs creates the directories mixer uses
|
|
func (b *Builder) initDirs() error {
|
|
// Create folder to store local rpms if defined but doesn't already exist
|
|
if b.Config.Mixer.LocalRPMDir != "" {
|
|
if err := os.MkdirAll(b.Config.Mixer.LocalRPMDir, 0777); err != nil {
|
|
return errors.Wrap(err, "Failed to create local rpms directory")
|
|
}
|
|
}
|
|
|
|
// Create folder for local dnf repo if defined but doesn't already exist
|
|
if b.Config.Mixer.LocalRepoDir != "" {
|
|
if err := os.MkdirAll(b.Config.Mixer.LocalRepoDir, 0777); err != nil {
|
|
return errors.Wrap(err, "Failed to create local rpms directory")
|
|
}
|
|
}
|
|
|
|
// Create folder for local bundle files
|
|
if err := os.MkdirAll(b.Config.Mixer.LocalBundleDir, 0777); err != nil {
|
|
return errors.Wrap(err, "Failed to create local bundles directory")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get latest published upstream version
|
|
func (b *Builder) getLatestUpstreamVersion() (string, error) {
|
|
ver, err := b.DownloadFileFromUpstreamAsString("/latest")
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "Failed to retrieve latest published upstream version. Missing proxy configuration? ")
|
|
}
|
|
|
|
return ver, nil
|
|
}
|
|
|
|
// buildUpstreamURL builds the full upstream URL based on a b.UpstreamURL and a
|
|
// supplied subpath
|
|
func (b *Builder) buildUpstreamURL(subpath string) (string, error) {
|
|
base, err := url.Parse(b.UpstreamURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, token := range strings.Split(subpath, "/") {
|
|
base.Path = path.Join(base.Path, token)
|
|
}
|
|
|
|
return base.String(), nil
|
|
}
|
|
|
|
// DownloadFileFromUpstreamAsString will download a file from the Upstream URL
|
|
// joined with the passed subpath. It will trim leading and trailing whitespace
|
|
// from the result.
|
|
func (b *Builder) DownloadFileFromUpstreamAsString(subpath string) (string, error) {
|
|
if b.UpstreamURL == "" {
|
|
return b.State.Mix.Format, nil
|
|
}
|
|
url, err := b.buildUpstreamURL(subpath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
content, err := helpers.DownloadFileAsString(url)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.TrimSpace(string(content)), nil
|
|
}
|
|
|
|
// DownloadFileFromUpstream will download a file from the Upstream URL
|
|
// joined with the passed subpath and write that file to the supplied file path.
|
|
// If the path is left empty, the file name will be inferred from the source
|
|
// and written to PWD.
|
|
func (b *Builder) DownloadFileFromUpstream(subpath string, filePath string) error {
|
|
url, err := b.buildUpstreamURL(subpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return helpers.DownloadFile(url, filePath)
|
|
}
|
|
|
|
const mixDirGitIgnore = `upstream-bundles/
|
|
mix-bundles/`
|
|
|
|
// InitMix will initialise a new swupd-client consumable "mix" with the given
|
|
// based Clear Linux version and specified mix version.
|
|
func (b *Builder) InitMix(upstreamVer string, mixVer string, allLocal bool, allUpstream bool, noDefaults bool, upstreamURL string, git bool) error {
|
|
// Set up local dirs
|
|
if err := b.initDirs(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set up mix metadata
|
|
// Deprecate '.clearurl' --> 'upstreamurl'
|
|
if _, err := os.Stat(filepath.Join(b.Config.Builder.VersionPath, ".clearurl")); err == nil {
|
|
b.UpstreamURLFile = ".clearurl"
|
|
fmt.Println("Warning: '.clearurl' has been deprecated. Please rename file to 'upstreamurl'")
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(b.Config.Builder.VersionPath, b.UpstreamURLFile), []byte(upstreamURL), 0644); err != nil {
|
|
return err
|
|
}
|
|
b.UpstreamURL = upstreamURL
|
|
|
|
if upstreamVer == "latest" {
|
|
ver, err := b.getLatestUpstreamVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
upstreamVer = ver
|
|
}
|
|
|
|
fmt.Printf("Initializing mix version %s from upstream version %s\n", mixVer, upstreamVer)
|
|
|
|
// Deprecate '.clearversion' --> 'upstreamversion'
|
|
if _, err := os.Stat(filepath.Join(b.Config.Builder.VersionPath, ".clearversion")); err == nil {
|
|
b.UpstreamVerFile = ".clearversion"
|
|
fmt.Println("Warning: '.clearversion' has been deprecated. Please rename file to 'upstreamversion'")
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(b.Config.Builder.VersionPath, b.UpstreamVerFile), []byte(upstreamVer), 0644); err != nil {
|
|
return err
|
|
}
|
|
b.UpstreamVer = upstreamVer
|
|
|
|
// Deprecate '.mixversion' --> 'mixversion'
|
|
if _, err := os.Stat(filepath.Join(b.Config.Builder.VersionPath, ".mixversion")); err == nil {
|
|
b.MixVerFile = ".mixversion"
|
|
fmt.Println("Warning: '.mixversion' has been deprecated. Please rename file to 'mixversion'")
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(b.Config.Builder.VersionPath, b.MixVerFile), []byte(mixVer), 0644); err != nil {
|
|
return err
|
|
}
|
|
b.MixVer = mixVer
|
|
|
|
// Parse strings into valid version numbers.
|
|
var err error
|
|
b.MixVerUint32, err = parseUint32(b.MixVer)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Couldn't parse mix version")
|
|
}
|
|
b.UpstreamVerUint32, err = parseUint32(b.UpstreamVer)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Couldn't parse upstream version")
|
|
}
|
|
|
|
// When running in offline mode, there is no upstream to get the default bundles from,
|
|
// so the mix must be created without the default bundles.
|
|
if Offline && !noDefaults {
|
|
fmt.Println("Running in offline mode. Forcing --no-default-bundles")
|
|
noDefaults = true
|
|
}
|
|
|
|
// Initialize the Mix Bundles List
|
|
var bundles []string
|
|
if !noDefaults {
|
|
bundles = []string{"os-core", "os-core-update", "bootloader", "kernel-native"}
|
|
}
|
|
if err := b.AddBundles(bundles, allLocal, allUpstream, false); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get upstream bundles
|
|
if err := b.getUpstreamBundles(upstreamVer, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
if git {
|
|
if err := ioutil.WriteFile(".gitignore", []byte(mixDirGitIgnore), 0644); err != nil {
|
|
return errors.Wrap(err, "Failed to create .gitignore file")
|
|
}
|
|
|
|
// Init repo and add initial commit
|
|
if err := helpers.Git("init"); err != nil {
|
|
return err
|
|
}
|
|
if err := helpers.Git("add", "."); err != nil {
|
|
return err
|
|
}
|
|
commitMsg := fmt.Sprintf("Initial mix version %s from upstream version %s", b.MixVer, b.UpstreamVer)
|
|
if err := helpers.Git("commit", "-m", commitMsg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadVersions will initialise the mix versions (mix and clearlinux) from
|
|
// the configuration files in the version directory.
|
|
func (b *Builder) ReadVersions() error {
|
|
// Deprecate '.mixversion' --> 'mixversion'
|
|
if _, err := os.Stat(filepath.Join(b.Config.Builder.VersionPath, ".mixversion")); err == nil {
|
|
b.MixVerFile = ".mixversion"
|
|
fmt.Println("Warning: '.mixversion' has been deprecated. Please rename file to 'mixversion'")
|
|
}
|
|
ver, err := ioutil.ReadFile(filepath.Join(b.Config.Builder.VersionPath, b.MixVerFile))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.MixVer = strings.TrimSpace(string(ver))
|
|
b.MixVer = strings.Replace(b.MixVer, "\n", "", -1)
|
|
|
|
// Deprecate '.clearversion' --> 'upstreamversion'
|
|
if _, err = os.Stat(filepath.Join(b.Config.Builder.VersionPath, ".clearversion")); err == nil {
|
|
b.UpstreamVerFile = ".clearversion"
|
|
fmt.Println("Warning: '.clearversion' has been deprecated. Please rename file to 'upstreamversion'")
|
|
}
|
|
ver, err = ioutil.ReadFile(filepath.Join(b.Config.Builder.VersionPath, b.UpstreamVerFile))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.UpstreamVer = strings.TrimSpace(string(ver))
|
|
b.UpstreamVer = strings.Replace(b.UpstreamVer, "\n", "", -1)
|
|
|
|
// Deprecate '.clearversion' --> 'upstreamurl'
|
|
if _, err = os.Stat(filepath.Join(b.Config.Builder.VersionPath, ".clearurl")); err == nil {
|
|
b.UpstreamURLFile = ".clearurl"
|
|
fmt.Println("Warning: '.clearurl' has been deprecated. Please rename file to 'upstreamurl'")
|
|
}
|
|
ver, err = ioutil.ReadFile(filepath.Join(b.Config.Builder.VersionPath, b.UpstreamURLFile))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "WARNING: %s/%s does not exist, run mixer init to generate\n", b.Config.Builder.VersionPath, b.UpstreamURLFile)
|
|
b.UpstreamURL = ""
|
|
} else {
|
|
b.UpstreamURL = strings.TrimSpace(string(ver))
|
|
b.UpstreamURL = strings.Replace(b.UpstreamURL, "\n", "", -1)
|
|
}
|
|
|
|
// Parse strings into valid version numbers.
|
|
b.MixVerUint32, err = parseUint32(b.MixVer)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Couldn't parse mix version")
|
|
}
|
|
b.UpstreamVerUint32, err = parseUint32(b.UpstreamVer)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Couldn't parse upstream version")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SignManifestMoM will sign the Manifest.MoM file in in place based on the Mix
|
|
// version read from builder.conf.
|
|
func (b *Builder) SignManifestMoM() error {
|
|
mom := filepath.Join(b.Config.Builder.ServerStateDir, "www", b.MixVer, "Manifest.MoM")
|
|
sig := mom + ".sig"
|
|
|
|
// Call openssl because signing and pkcs7 stuff is not well supported in Go yet.
|
|
cmd := exec.Command("openssl", "smime", "-sign", "-binary", "-in", mom,
|
|
"-signer", b.Config.Builder.Cert, "-inkey", filepath.Dir(b.Config.Builder.Cert)+"/private.pem",
|
|
"-outform", "DER", "-out", sig)
|
|
|
|
// Capture the output as it is useful in case of errors.
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to sign Manifest.MoM:\n%s", out.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
upstreamBundlesBaseDir = "upstream-bundles"
|
|
upstreamBundlesVerDirFmt = "clr-bundles-%s"
|
|
upstreamBundlesBundleDir = "bundles"
|
|
)
|
|
|
|
func getUpstreamBundlesVerDir(ver string) string {
|
|
return fmt.Sprintf(upstreamBundlesVerDirFmt, ver)
|
|
}
|
|
func getUpstreamBundlesPath(ver string) string {
|
|
return filepath.Join(upstreamBundlesBaseDir, fmt.Sprintf(upstreamBundlesVerDirFmt, ver), upstreamBundlesBundleDir)
|
|
}
|
|
|
|
func (b *Builder) getLocalPackagesPath() string {
|
|
return filepath.Join(b.Config.Builder.VersionPath, b.LocalPackagesFile)
|
|
}
|
|
|
|
func (b *Builder) getUpstreamPackagesPath() string {
|
|
return filepath.Join(upstreamBundlesBaseDir, getUpstreamBundlesVerDir(b.UpstreamVer), "packages")
|
|
}
|
|
|
|
func (b *Builder) getUpstreamBundles(ver string, prune bool) error {
|
|
if Offline {
|
|
return nil
|
|
}
|
|
|
|
// Make the folder to store upstream bundles if does not exist
|
|
if err := os.MkdirAll(upstreamBundlesBaseDir, 0777); err != nil {
|
|
return errors.Wrap(err, "Failed to create upstream-bundles dir.")
|
|
}
|
|
|
|
bundleDir := getUpstreamBundlesPath(ver)
|
|
|
|
// Clear out other bundle dirs if needed
|
|
if prune {
|
|
files, err := helpers.ListVisibleFiles(upstreamBundlesBaseDir)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to read bundles for pruning.")
|
|
}
|
|
curver := getUpstreamBundlesVerDir(ver)
|
|
for _, file := range files {
|
|
// Skip the current version cache, but delete all others
|
|
if file != curver {
|
|
if err = os.RemoveAll(filepath.Join(upstreamBundlesBaseDir, file)); err != nil {
|
|
return errors.Wrapf(err, "Failed to remove %s while pruning bundles", file)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if bundles already exist
|
|
if _, err := os.Stat(bundleDir); err == nil {
|
|
return nil
|
|
}
|
|
|
|
tmptarfile := filepath.Join(upstreamBundlesBaseDir, ver+".tar.gz")
|
|
URL := "https://github.com/clearlinux/clr-bundles/archive/" + ver + ".tar.gz"
|
|
if err := helpers.DownloadFile(URL, tmptarfile); err != nil {
|
|
return errors.Wrapf(err, "Failed to download bundles for upstream version %s", ver)
|
|
}
|
|
|
|
if err := helpers.UnpackFile(tmptarfile, upstreamBundlesBaseDir); err != nil {
|
|
err = errors.Wrapf(err, "Error unpacking bundles for upstream version %s\n%s left for debuging", ver, tmptarfile)
|
|
|
|
// Clean up upstream bundle dir, since unpack failed
|
|
path := filepath.Join(upstreamBundlesBaseDir, getUpstreamBundlesVerDir(ver))
|
|
if rerr := os.RemoveAll(path); rerr != nil {
|
|
err = errors.Wrapf(err, "Error cleaning up upstream bundle dir: %s", path)
|
|
}
|
|
return err
|
|
}
|
|
|
|
return errors.Wrapf(os.Remove(tmptarfile), "Failed to remove temp bundle archive: %s", tmptarfile)
|
|
}
|
|
|
|
func setPackagesList(source *map[string]bool, filename string) error {
|
|
var err error
|
|
if len(*source) > 0 {
|
|
return nil
|
|
}
|
|
if _, err = os.Stat(filename); os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
|
|
*source, err = parsePackageBundleFile(filename)
|
|
return err
|
|
}
|
|
|
|
// getBundlePath returns the path to the bundle definition file for a given
|
|
// bundle name, or error if it cannot be found. Looks in the following order:
|
|
// local-bundles/
|
|
// local-packages
|
|
// upstream-bundles/clr-bundles-<ver>/bundles/
|
|
// upstream-bundles/clr-bundles-<ver>/packages
|
|
func (b *Builder) getBundlePath(bundle string) (string, error) {
|
|
// Check local-bundles
|
|
path := filepath.Join(b.Config.Mixer.LocalBundleDir, bundle)
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path, nil
|
|
}
|
|
|
|
// Check local-packages
|
|
path = b.getLocalPackagesPath()
|
|
err := setPackagesList(&localPackages, path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, ok := localPackages[bundle]; ok {
|
|
return path, nil
|
|
}
|
|
|
|
// Check upstream-bundles
|
|
path = filepath.Join(getUpstreamBundlesPath(b.UpstreamVer), bundle)
|
|
if _, err = os.Stat(path); err == nil {
|
|
return path, nil
|
|
}
|
|
|
|
// Check upstream-packages
|
|
path = b.getUpstreamPackagesPath()
|
|
err = setPackagesList(&upstreamPackages, path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, ok := upstreamPackages[bundle]; ok {
|
|
return path, nil
|
|
}
|
|
|
|
return "", errors.Errorf("Cannot find bundle %q in local or upstream bundles", bundle)
|
|
}
|
|
|
|
// isLocalBundle checks to see if a bundle filepath is a local bundle definition or package file
|
|
func (b *Builder) isLocalBundle(path string) bool {
|
|
return strings.HasPrefix(path, b.Config.Mixer.LocalBundleDir) || b.isLocalPackagePath(path)
|
|
}
|
|
|
|
func getBundleSetKeys(set bundleSet) []string {
|
|
keys := make([]string, len(set))
|
|
i := 0
|
|
for k := range set {
|
|
keys[i] = k
|
|
i++
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func getBundleSetKeysSorted(set bundleSet) []string {
|
|
keys := getBundleSetKeys(set)
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// isLocalPackagePath checks if path is a local-packages definition file
|
|
func (b *Builder) isLocalPackagePath(path string) bool {
|
|
return strings.HasSuffix(path, b.LocalPackagesFile)
|
|
}
|
|
|
|
// isUpstreamPackagePath checks if path is an upstream packages definition file
|
|
func isUpstreamPackagePath(path string) bool {
|
|
return strings.HasSuffix(path, "/packages")
|
|
}
|
|
|
|
// isPathToPackageFile checks if the path is a local or upstream package definition file
|
|
func (b *Builder) isPathToPackageFile(path string) bool {
|
|
return b.isLocalPackagePath(path) || isUpstreamPackagePath(path)
|
|
}
|
|
|
|
func (b *Builder) getBundleFromName(name string) (*bundle, error) {
|
|
var bundle *bundle
|
|
path, err := b.getBundlePath(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if b.isPathToPackageFile(path) {
|
|
return newBundleFromPackage(name, path)
|
|
}
|
|
|
|
bundle, err = parseBundleFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = validateBundleFilename(path); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return bundle, nil
|
|
}
|
|
|
|
// getMixBundlesListAsSet reads in the Mix Bundles List file and returns the
|
|
// resultant set of unique bundle objects. If the mix bundles file does not
|
|
// exist or is empty, an empty set is returned.
|
|
func (b *Builder) getMixBundlesListAsSet() (bundleSet, error) {
|
|
set := make(bundleSet)
|
|
|
|
bundles, err := helpers.ReadFileAndSplit(filepath.Join(b.Config.Builder.VersionPath, b.MixBundlesFile))
|
|
if os.IsNotExist(err) {
|
|
return set, nil
|
|
} else if err != nil {
|
|
return nil, errors.Wrap(err, "Failed to read in Mix Bundle List")
|
|
}
|
|
|
|
for _, bName := range bundles {
|
|
bName = strings.TrimSpace(bName)
|
|
if bName == "" {
|
|
continue
|
|
}
|
|
|
|
bundle, err := b.getBundleFromName(bName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
set[bName] = bundle
|
|
}
|
|
return set, nil
|
|
}
|
|
|
|
// getDirBundlesListAsSet reads the files in a directory and returns the
|
|
// resultant set of unique bundle objects. If the directory is empty, an empty
|
|
// set is returned.
|
|
func (b *Builder) getDirBundlesListAsSet(dir string) (bundleSet, error) {
|
|
set := make(bundleSet)
|
|
|
|
files, err := helpers.ListVisibleFiles(dir)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "Failed to read bundles dir: %s", dir)
|
|
}
|
|
|
|
for _, file := range files {
|
|
bundle, err := b.getBundleFromName(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
set[file] = bundle
|
|
}
|
|
return set, nil
|
|
}
|
|
|
|
// writeMixBundleList writes the contents of a bundle set out to the Mix Bundles
|
|
// List file. Values will be in sorted order.
|
|
func (b *Builder) writeMixBundleList(set bundleSet) error {
|
|
data := []byte(strings.Join(getBundleSetKeysSorted(set), "\n") + "\n")
|
|
if err := ioutil.WriteFile(filepath.Join(b.Config.Builder.VersionPath, b.MixBundlesFile), data, 0644); err != nil {
|
|
return errors.Wrap(err, "Failed to write out Mix Bundle List")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getFullBundleSet takes a set of bundle names to traverse, and returns a full
|
|
// set of recursively-parsed bundle objects.
|
|
func (b *Builder) getFullBundleSet(bundles bundleSet) (bundleSet, error) {
|
|
set := make(bundleSet)
|
|
|
|
// recurseBundleSet adds a list of bundles to a bundle set,
|
|
// recursively adding any bundles included by those in the list.
|
|
var recurseBundleSet func(bundles []string) error
|
|
recurseBundleSet = func(bundles []string) error {
|
|
for _, bName := range bundles {
|
|
if _, exists := set[bName]; !exists {
|
|
bundle, err := b.getBundleFromName(bName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
set[bName] = bundle
|
|
|
|
if len(bundle.DirectIncludes) > 0 {
|
|
err := recurseBundleSet(bundle.DirectIncludes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err := recurseBundleSet(getBundleSetKeys(bundles))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return set, nil
|
|
}
|
|
|
|
func populateSetFromPackages(source *map[string]bool, dest bundleSet, filename string) error {
|
|
var err error
|
|
err = setPackagesList(source, filename)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Failed to read packages file: %s", filename)
|
|
}
|
|
for k := range *source {
|
|
if _, ok := dest[k]; ok {
|
|
fmt.Printf("Bundle %q already in mix; skipping\n", k)
|
|
continue
|
|
}
|
|
dest[k], err = newBundleFromPackage(k, filename)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Failed to add %q bundle to mix", k)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getFullMixBundleSet returns the full set of mix bundle objects. It is a
|
|
// convenience function that is equivalent to calling getFullBundleSet on the
|
|
// results of getMixBundlesListAsSet.
|
|
func (b *Builder) getFullMixBundleSet() (bundleSet, error) {
|
|
bundles, err := b.getMixBundlesListAsSet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
set, err := b.getFullBundleSet(bundles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return set, nil
|
|
}
|
|
|
|
// AddBundles adds the specified bundles to the Mix Bundles List. Values are
|
|
// verified as valid, and duplicate values are removed. The resulting Mix
|
|
// Bundles List will be in sorted order.
|
|
func (b *Builder) AddBundles(bundles []string, allLocal bool, allUpstream bool, git bool) error {
|
|
// Fetch upstream bundle files if needed
|
|
if err := b.getUpstreamBundles(b.UpstreamVer, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Read in current mix bundles list
|
|
set, err := b.getMixBundlesListAsSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the ones passed in to the set
|
|
for _, bName := range bundles {
|
|
if _, exists := set[bName]; exists {
|
|
fmt.Printf("Bundle %q already in mix; skipping\n", bName)
|
|
continue
|
|
}
|
|
|
|
bundle, err := b.getBundleFromName(bName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if b.isLocalBundle(bundle.Filename) {
|
|
fmt.Printf("Adding bundle %q from local bundles\n", bName)
|
|
} else {
|
|
fmt.Printf("Adding bundle %q from upstream bundles\n", bName)
|
|
}
|
|
set[bName] = bundle
|
|
}
|
|
|
|
// Add all local bundles to the bundles
|
|
if allLocal {
|
|
localSet, err := b.getDirBundlesListAsSet(b.Config.Mixer.LocalBundleDir)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Failed to read local bundles dir: %s", b.Config.Mixer.LocalBundleDir)
|
|
}
|
|
// handle packages defined in local-packages, if it exists
|
|
err = populateSetFromPackages(&localPackages, localSet, b.getLocalPackagesPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, bundle := range localSet {
|
|
if _, exists := set[bundle.Name]; exists {
|
|
fmt.Printf("Bundle %q already in mix; skipping\n", bundle.Name)
|
|
continue
|
|
}
|
|
|
|
set[bundle.Name] = bundle
|
|
fmt.Printf("Adding bundle %q from local bundles\n", bundle.Name)
|
|
}
|
|
}
|
|
|
|
// Add all upstream bundles to the bundles
|
|
if allUpstream {
|
|
upstreamBundleDir := getUpstreamBundlesPath(b.UpstreamVer)
|
|
upstreamSet, err := b.getDirBundlesListAsSet(upstreamBundleDir)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Failed to read upstream bundles dir: %s", upstreamBundleDir)
|
|
}
|
|
// handle packages defined in upstream packages file, if it exists
|
|
err = populateSetFromPackages(&upstreamPackages, upstreamSet, b.getUpstreamPackagesPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, bundle := range upstreamSet {
|
|
if _, exists := set[bundle.Name]; exists {
|
|
fmt.Printf("Bundle %q already in mix; skipping\n", bundle.Name)
|
|
continue
|
|
}
|
|
|
|
set[bundle.Name] = bundle
|
|
fmt.Printf("Adding bundle %q from upstream bundles\n", bundle.Name)
|
|
}
|
|
}
|
|
|
|
// Write final mix bundle list back to file
|
|
if err := b.writeMixBundleList(set); err != nil {
|
|
return err
|
|
}
|
|
|
|
if git {
|
|
fmt.Println("Adding git commit")
|
|
if err := helpers.Git("add", "."); err != nil {
|
|
return err
|
|
}
|
|
commitMsg := fmt.Sprintf("Added bundles from local-bundles or upstream version %s\n\nBundles added: %v", b.UpstreamVer, bundles)
|
|
if err := helpers.Git("commit", "-q", "-m", commitMsg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveBundles removes a list of bundles from the Mix Bundles List. If a
|
|
// bundle is not present, it is skipped. If 'local' is passed, the corresponding
|
|
// bundle file is removed from local-bundles. Note that this is an irreversible
|
|
// step. The Mix Bundles List is validated when read in, and the resulting Mix
|
|
// Bundles List will be in sorted order.
|
|
func (b *Builder) RemoveBundles(bundles []string, mix bool, local bool, git bool) error {
|
|
// Fetch upstream bundle files if needed
|
|
if err := b.getUpstreamBundles(b.UpstreamVer, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Read in current mix bundles list
|
|
set, err := b.getMixBundlesListAsSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove the ones passed in from the set
|
|
for _, bundle := range bundles {
|
|
_, inMix := set[bundle]
|
|
|
|
if local {
|
|
if _, err := os.Stat(filepath.Join(b.Config.Mixer.LocalBundleDir, bundle)); err == nil {
|
|
fmt.Printf("Removing bundle file for %q from local-bundles\n", bundle)
|
|
if err := os.Remove(filepath.Join(b.Config.Mixer.LocalBundleDir, bundle)); err != nil {
|
|
return errors.Wrapf(err, "Cannot remove bundle file for %q from local-bundles", bundle)
|
|
}
|
|
|
|
if !mix && inMix {
|
|
// Check if bundle is still available upstream
|
|
if _, err := b.getBundlePath(bundle); err != nil {
|
|
fmt.Printf("Warning: Invalid bundle left in mix: %q\n", bundle)
|
|
} else {
|
|
fmt.Printf("Mix bundle %q now points to upstream\n", bundle)
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Printf("Bundle %q not found in local-bundles; skipping\n", bundle)
|
|
}
|
|
}
|
|
|
|
if mix {
|
|
if inMix {
|
|
fmt.Printf("Removing bundle %q from mix\n", bundle)
|
|
delete(set, bundle)
|
|
} else {
|
|
fmt.Printf("Bundle %q not found in mix; skipping\n", bundle)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write final mix bundle list back to file, only if the Mix Bundle List was edited
|
|
if mix {
|
|
if err := b.writeMixBundleList(set); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if git {
|
|
fmt.Println("Adding git commit")
|
|
if err := helpers.Git("add", "."); err != nil {
|
|
return err
|
|
}
|
|
commitMsg := fmt.Sprintf("Bundles removed: %v", bundles)
|
|
if err := helpers.Git("commit", "-q", "-m", commitMsg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
// Using the Unicode "Box Drawing" group
|
|
treeNil = " "
|
|
treeBar = "│ "
|
|
treeMid = "├── "
|
|
treeEnd = "└── "
|
|
)
|
|
|
|
func (b *Builder) buildTreePrintValue(bundle *bundle, level int, levelEnded []bool) string {
|
|
// Set up the value for this bundle
|
|
value := bundle.Name
|
|
if b.isLocalBundle(bundle.Filename) {
|
|
if b.isLocalPackagePath(bundle.Filename) {
|
|
value += " (local package)"
|
|
} else {
|
|
value += " (local bundle)"
|
|
}
|
|
} else {
|
|
if isUpstreamPackagePath(bundle.Filename) {
|
|
value += " (upstream package)"
|
|
} else {
|
|
value += " (upstream bundle)"
|
|
}
|
|
}
|
|
|
|
if level == 0 {
|
|
return value
|
|
}
|
|
|
|
var buff bytes.Buffer
|
|
// Add continuation bars if earlier levels have not ended
|
|
for i := 0; i < level-1; i++ {
|
|
if levelEnded[i] {
|
|
buff.WriteString(treeNil)
|
|
} else {
|
|
buff.WriteString(treeBar)
|
|
}
|
|
}
|
|
|
|
// Add a mid bar or an end bar
|
|
if levelEnded[level-1] {
|
|
buff.WriteString(treeEnd)
|
|
} else {
|
|
buff.WriteString(treeMid)
|
|
}
|
|
|
|
// Add the actual value
|
|
buff.WriteString(value)
|
|
|
|
return buff.String()
|
|
}
|
|
|
|
func (b *Builder) bundleTreePrint(set bundleSet, bundle string, level int, levelEnded []bool) {
|
|
fmt.Println(b.buildTreePrintValue(set[bundle], level, levelEnded))
|
|
|
|
levelEnded = append(levelEnded, false)
|
|
last := len(set[bundle].DirectIncludes) - 1
|
|
for i, include := range set[bundle].DirectIncludes {
|
|
levelEnded[level] = i == last
|
|
b.bundleTreePrint(set, include, level+1, levelEnded)
|
|
}
|
|
}
|
|
|
|
type listType int
|
|
|
|
// Enum of available list types
|
|
const (
|
|
MixList listType = iota // List bundles in the mix (with includes)
|
|
LocalList // List bundles available locally
|
|
UpstreamList // List bundles available upstream
|
|
)
|
|
|
|
// ListBundles prints out a bundle list in either a flat list or tree view
|
|
func (b *Builder) ListBundles(listType listType, tree bool) error {
|
|
// Fetch upstream bundle files if needed
|
|
if err := b.getUpstreamBundles(b.UpstreamVer, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
var bundles bundleSet
|
|
|
|
// Get the bundle sets used for processing
|
|
mixBundles, err := b.getMixBundlesListAsSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
localBundles, err := b.getDirBundlesListAsSet(b.Config.Mixer.LocalBundleDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// handle packages defined in local-packages, if it exists
|
|
err = populateSetFromPackages(&localPackages, localBundles, b.getLocalPackagesPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
upstreamBundles, err := b.getDirBundlesListAsSet(getUpstreamBundlesPath(b.UpstreamVer))
|
|
if err != nil {
|
|
if !Offline {
|
|
return err
|
|
}
|
|
upstreamBundles = make(bundleSet)
|
|
}
|
|
// handle packages defined in upstream packages file, if it exists
|
|
err = populateSetFromPackages(&upstreamPackages, upstreamBundles, b.getUpstreamPackagesPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Assign "top level" bundles
|
|
switch listType {
|
|
case MixList:
|
|
bundles = mixBundles
|
|
case LocalList:
|
|
bundles = localBundles
|
|
case UpstreamList:
|
|
bundles = upstreamBundles
|
|
}
|
|
|
|
// Create the full, parsed set
|
|
set, err := b.getFullBundleSet(bundles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if tree {
|
|
// Print the tree view
|
|
sorted := getBundleSetKeysSorted(bundles)
|
|
for _, bundle := range sorted {
|
|
b.bundleTreePrint(set, bundle, 0, make([]bool, 0))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
|
|
|
// Print a flat list and return
|
|
switch listType {
|
|
case MixList:
|
|
// Print the full, parsed set
|
|
sorted := getBundleSetKeysSorted(set)
|
|
for _, bundle := range sorted {
|
|
var location string
|
|
if _, exists := localBundles[bundle]; exists {
|
|
if b.isLocalPackagePath(localBundles[bundle].Filename) {
|
|
location = "(local package)"
|
|
} else {
|
|
location = "(local bundle)"
|
|
}
|
|
} else {
|
|
if isUpstreamPackagePath(upstreamBundles[bundle].Filename) {
|
|
location = "(upstream package)"
|
|
} else {
|
|
location = "(upstream bundle)"
|
|
}
|
|
}
|
|
var included string
|
|
if _, exists := bundles[bundle]; !exists {
|
|
included = "(included)"
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\n", bundle, location, included)
|
|
}
|
|
case LocalList:
|
|
// Only print the top-level set
|
|
sorted := getBundleSetKeysSorted(bundles)
|
|
for _, bundle := range sorted {
|
|
var mix string
|
|
if _, exists := mixBundles[bundle]; exists {
|
|
mix = "(in mix)"
|
|
}
|
|
var pkg string
|
|
if b.isLocalPackagePath(localBundles[bundle].Filename) {
|
|
pkg = "(package)"
|
|
}
|
|
var masking string
|
|
if _, exists := upstreamBundles[bundle]; exists {
|
|
masking = "(masking upstream)"
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", bundle, pkg, mix, masking)
|
|
}
|
|
case UpstreamList:
|
|
// Only print the top-level set
|
|
sorted := getBundleSetKeysSorted(bundles)
|
|
for _, bundle := range sorted {
|
|
var mix string
|
|
if _, exists := mixBundles[bundle]; exists {
|
|
mix = "(in mix)"
|
|
}
|
|
var pkg string
|
|
if isUpstreamPackagePath(upstreamBundles[bundle].Filename) {
|
|
pkg = "(package)"
|
|
}
|
|
var masked string
|
|
if _, exists := localBundles[bundle]; exists {
|
|
masked = "(masked by local)"
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", bundle, pkg, mix, masked)
|
|
}
|
|
}
|
|
|
|
_ = tw.Flush()
|
|
|
|
return nil
|
|
}
|
|
|
|
func getEditorCmd() (string, error) {
|
|
cmd := os.Getenv("VISUAL")
|
|
if cmd != "" {
|
|
return cmd, nil
|
|
}
|
|
|
|
cmd = os.Getenv("EDITOR")
|
|
if cmd != "" {
|
|
return cmd, nil
|
|
}
|
|
|
|
return exec.LookPath("nano")
|
|
}
|
|
|
|
// editBundleFile launches an editor command to edit the bundle defined by path.
|
|
// When the edit process ends, the bundle file is parsed for validity. If a
|
|
// parsing error is encountered, the user is asked how to proceed: retry, revert
|
|
// and retry, or skip.
|
|
func editBundleFile(editorCmd string, bundle string, path string) error {
|
|
// Make backup
|
|
backup := path + ".orig"
|
|
if err := helpers.CopyFileNoOverwrite(backup, path); err != nil && !os.IsExist(err) {
|
|
return errors.Wrapf(err, "Could not backup bundle %q file for editing", bundle)
|
|
}
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
revert := false
|
|
|
|
editLoop:
|
|
for {
|
|
if revert {
|
|
if err := helpers.CopyFile(path, backup); err != nil {
|
|
return errors.Wrapf(err, "Could not restore original from backup for bundle %q", bundle)
|
|
}
|
|
}
|
|
|
|
// Ignore return from command; parsing below is what will reveal errors
|
|
_ = helpers.RunCommandInput(os.Stdin, editorCmd, path)
|
|
|
|
err := validateBundleFile(path, BasicValidation)
|
|
if err == nil {
|
|
// Clean-up backup
|
|
if err = os.Remove(backup); err != nil {
|
|
return errors.Wrapf(err, "Error cleaning up backup for bundle %q", bundle)
|
|
}
|
|
break editLoop
|
|
}
|
|
|
|
fmt.Printf("Error parsing bundle %q: %s\n", bundle, err)
|
|
for {
|
|
// Ask the user if they want to retry, revert, or skip
|
|
fmt.Print("Would you like to edit as-is, revert and edit, or skip [Edit/Revert/Skip]?: ")
|
|
text, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Error reading input")
|
|
}
|
|
text = strings.ToLower(text)
|
|
text = strings.TrimSpace(text)
|
|
switch text {
|
|
case "e", "edit":
|
|
revert = false
|
|
continue editLoop
|
|
case "r", "revert":
|
|
revert = true
|
|
continue editLoop
|
|
case "s", "skip":
|
|
fmt.Printf("Skipping bundle %q despite errors. Backup retained as %q\n", bundle, bundle+".orig")
|
|
break editLoop
|
|
default:
|
|
fmt.Printf("Invalid input: %q", text)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const bundleTemplateFormat = `# [TITLE]: %s
|
|
# [DESCRIPTION]:
|
|
# [STATUS]:
|
|
# [CAPABILITIES]:
|
|
# [MAINTAINER]:
|
|
#
|
|
# List bundles one per line. Includes have format: include(bundle)
|
|
`
|
|
|
|
func createBundleFile(bundle string, path string) error {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0666)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data := []byte(fmt.Sprintf(bundleTemplateFormat, bundle))
|
|
_, err = f.Write(data)
|
|
_ = f.Close()
|
|
return err
|
|
}
|
|
|
|
// EditBundles copies a list of bundles from upstream-bundles to local-bundles
|
|
// (if they are not already there) or creates a blank template if they are new,
|
|
// and launches an editor to edit them. Passing true for 'suppressEditor' will
|
|
// suppress the launching of the editor (and just do the copy or create, if
|
|
// needed), and 'add' will also add the bundles to the mix.
|
|
func (b *Builder) EditBundles(bundles []string, suppressEditor bool, add bool, git bool) error {
|
|
// Fetch upstream bundle files if needed
|
|
if err := b.getUpstreamBundles(b.UpstreamVer, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
editorCmd, err := getEditorCmd()
|
|
if err != nil {
|
|
fmt.Println("Cannot find a valid editor (see usage for configuration). Copying to local-bundles only.")
|
|
suppressEditor = true
|
|
}
|
|
|
|
for _, bundle := range bundles {
|
|
path, _ := b.getBundlePath(bundle)
|
|
if !b.isLocalBundle(path) {
|
|
localPath := filepath.Join(b.Config.Mixer.LocalBundleDir, bundle)
|
|
|
|
if path == "" {
|
|
// Bunlde not found upstream, so create new
|
|
if err = createBundleFile(bundle, localPath); err != nil {
|
|
return errors.Wrapf(err, "Failed to write bundle template for bundle %q", bundle)
|
|
}
|
|
} else {
|
|
// Bundle found upstream, so copy over
|
|
if err = helpers.CopyFile(localPath, path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
path = localPath
|
|
}
|
|
|
|
if suppressEditor {
|
|
continue
|
|
}
|
|
|
|
if err = editBundleFile(editorCmd, bundle, path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if add {
|
|
if err = b.AddBundles(bundles, false, false, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if git {
|
|
fmt.Println("Adding git commit")
|
|
if err := helpers.Git("add", "."); err != nil {
|
|
return err
|
|
}
|
|
commitMsg := fmt.Sprintf("Edited bundles: %v", bundles)
|
|
if err := helpers.Git("commit", "-q", "-m", commitMsg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateLocalBundles runs bundle parsing validation on all local bundles.
|
|
func (b *Builder) ValidateLocalBundles(lvl ValidationLevel) error {
|
|
files, err := helpers.ListVisibleFiles(b.Config.Mixer.LocalBundleDir)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to read local-bundles")
|
|
}
|
|
|
|
bundles := make([]string, len(files))
|
|
for i, file := range files {
|
|
bundles[i] = file
|
|
}
|
|
|
|
return b.ValidateBundles(bundles, lvl)
|
|
}
|
|
|
|
// ValidateBundles runs bundle parsing validation on a list of local bundles. In
|
|
// addition to parsing errors, errors are generated if the bundle is not found
|
|
// in local-bundles.
|
|
func (b *Builder) ValidateBundles(bundles []string, lvl ValidationLevel) error {
|
|
invalid := false
|
|
for _, bundle := range bundles {
|
|
path := filepath.Join(b.Config.Mixer.LocalBundleDir, bundle)
|
|
|
|
if err := validateBundleFile(path, lvl); err != nil {
|
|
invalid = true
|
|
fmt.Printf("Invalid: %q:\n%s\n\n", bundle, err)
|
|
}
|
|
}
|
|
|
|
if invalid {
|
|
return errors.New("Invalid bundles found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateMixVer sets the mix version in the builder object and writes it out to file
|
|
func (b *Builder) UpdateMixVer(version int) error {
|
|
// Deprecate '.mixversion' --> 'mixversion'
|
|
if _, err := os.Stat(filepath.Join(b.Config.Builder.VersionPath, ".mixversion")); err == nil {
|
|
b.MixVerFile = ".mixversion"
|
|
fmt.Println("Warning: '.mixversion' has been deprecated. Please rename file to 'mixversion'")
|
|
}
|
|
|
|
b.MixVer = strconv.Itoa(version)
|
|
b.MixVerUint32 = uint32(version)
|
|
return ioutil.WriteFile(filepath.Join(b.Config.Builder.VersionPath, b.MixVerFile), []byte(b.MixVer), 0644)
|
|
}
|
|
|
|
// If Base == true, template will include the [main] and [clear] sections.
|
|
// If Local == true, template will include the [local] section.
|
|
type dnfConf struct {
|
|
UpstreamURL, RepoDir string
|
|
Base, Local bool
|
|
}
|
|
|
|
const dnfConfTemplate = `{{if .Base}}[main]
|
|
cachedir=/var/cache/yum/clear/
|
|
keepcache=0
|
|
debuglevel=2
|
|
logfile=/var/log/yum.log
|
|
exactarch=1
|
|
obsoletes=1
|
|
gpgcheck=0
|
|
plugins=0
|
|
installonly_limit=3
|
|
reposdir=/root/mash
|
|
|
|
[clear]
|
|
name=Clear
|
|
failovermethod=priority
|
|
baseurl={{.UpstreamURL}}/releases/$releasever/clear/x86_64/os/
|
|
enabled=1
|
|
gpgcheck=0
|
|
{{end}}{{if .Local}}
|
|
[local]
|
|
name=Local
|
|
failovermethod=priority
|
|
baseurl=file://{{.RepoDir}}
|
|
enabled=1
|
|
gpgcheck=0
|
|
priority=1
|
|
{{end}}`
|
|
|
|
// NewDNFConfIfNeeded creates a new DNF configuration file if it does not already exist
|
|
func (b *Builder) NewDNFConfIfNeeded() error {
|
|
conf := dnfConf{
|
|
UpstreamURL: b.UpstreamURL,
|
|
RepoDir: b.Config.Mixer.LocalRepoDir,
|
|
}
|
|
|
|
if _, err := os.Stat(b.Config.Builder.DNFConf); os.IsNotExist(err) {
|
|
conf.Base = true
|
|
if b.Config.Mixer.LocalRepoDir != "" {
|
|
conf.Local = true
|
|
}
|
|
} else if err == nil && b.Config.Mixer.LocalRepoDir != "" {
|
|
// check if conf file contains local section
|
|
raw, err := ioutil.ReadFile(b.Config.Builder.DNFConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.Contains(string(raw), "[local]") {
|
|
conf.Local = true
|
|
}
|
|
}
|
|
|
|
if conf.Base || conf.Local {
|
|
f, err := os.OpenFile(b.Config.Builder.DNFConf, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_ = f.Close()
|
|
}()
|
|
|
|
t, err := textTemplate.New("dnfConfTemplate").Parse(dnfConfTemplate)
|
|
if err != nil {
|
|
helpers.PrintError(err)
|
|
return err
|
|
}
|
|
|
|
if err = t.Execute(f, conf); err != nil {
|
|
return errors.Wrapf(err, "Failed to write to dnf file: %s", b.Config.Builder.DNFConf)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getClosestAncestorOwner returns the owner uid/gid of the closest existing
|
|
// ancestor of the file or dir pointed to by path. There is no fully cross-
|
|
// platform concept of "ownership", so this method only works on *nix systems.
|
|
func getClosestAncestorOwner(path string) (int, int, error) {
|
|
path = filepath.Dir(path)
|
|
fi, err := os.Stat(path)
|
|
if os.IsNotExist(err) {
|
|
return getClosestAncestorOwner(path)
|
|
} else if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
uid := int(fi.Sys().(*syscall.Stat_t).Uid)
|
|
gid := int(fi.Sys().(*syscall.Stat_t).Gid)
|
|
return uid, gid, nil
|
|
}
|
|
|
|
// BuildBundles will attempt to construct the bundles required by generating a
|
|
// DNF configuration file, then resolving all files for each bundle using dnf
|
|
// resolve and no-op installs. One full chroot is created from this step with
|
|
// the file contents of all bundles.
|
|
func (b *Builder) BuildBundles(template *x509.Certificate, privkey *rsa.PrivateKey, signflag, clean bool) error {
|
|
// Fetch upstream bundle files if needed
|
|
if err := b.getUpstreamBundles(b.UpstreamVer, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Generate the dnf config file if it does not exist.
|
|
// This takes the template and adds the relevant local rpm repo path if needed
|
|
fmt.Println("Building bundles...")
|
|
|
|
timer := &stopWatch{w: os.Stdout}
|
|
defer timer.WriteSummary(os.Stdout)
|
|
|
|
timer.Start("BUILD BUNDLES")
|
|
if err := b.NewDNFConfIfNeeded(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If ServerStateDir does not exist, create it with ownership matching its
|
|
// closest existing ancestor directory
|
|
if _, err := os.Stat(b.Config.Builder.ServerStateDir); os.IsNotExist(err) {
|
|
uid, gid, err := getClosestAncestorOwner(b.Config.Builder.ServerStateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = os.MkdirAll(b.Config.Builder.ServerStateDir, 0755); err != nil {
|
|
return errors.Wrapf(err, "Failed to create server state dir: %q", b.Config.Builder.ServerStateDir)
|
|
}
|
|
if err = os.Chown(b.Config.Builder.ServerStateDir, uid, gid); err != nil {
|
|
return errors.Wrapf(err, "Failed to set ownership of dir: %q", b.Config.Builder.ServerStateDir)
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(b.Config.Builder.ServerStateDir + "/image/" + b.MixVer); err == nil && clean {
|
|
fmt.Printf("* Wiping away previous version %s...\n", b.MixVer)
|
|
err = os.RemoveAll(b.Config.Builder.ServerStateDir + "/www/" + b.MixVer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.RemoveAll(b.Config.Builder.ServerStateDir + "/image/" + b.MixVer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Generate the certificate needed for signing verification if it does not exist
|
|
if signflag == false && template != nil {
|
|
err := helpers.GenerateCertificate(b.Config.Builder.Cert, template, template, &privkey.PublicKey, privkey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Get the set of bundles to build
|
|
set, err := b.getFullMixBundleSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate set and compute AllPackages
|
|
if err = validateAndFillBundleSet(set); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Merge the rest of this function into buildBundles (or vice-versa).
|
|
err = b.buildBundles(set)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Move this logic to code that uses this.
|
|
// If LAST_VER don't exists, it means this is the first bundle we build,
|
|
// so initialize it to version "0".
|
|
lastVerPath := filepath.Join(b.Config.Builder.ServerStateDir, "image", "LAST_VER")
|
|
if _, err = os.Stat(lastVerPath); os.IsNotExist(err) {
|
|
err = ioutil.WriteFile(lastVerPath, []byte("0\n"), 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
timer.Stop()
|
|
|
|
return nil
|
|
}
|
|
|
|
// BuildUpdate will produce an update consumable by the swupd client
|
|
func (b *Builder) BuildUpdate(params UpdateParameters) error {
|
|
var err error
|
|
|
|
if params.MinVersion < 0 || params.MinVersion > math.MaxUint32 {
|
|
return errors.Errorf("minVersion %d is out of range", params.MinVersion)
|
|
}
|
|
|
|
if params.Format != "" {
|
|
b.State.Mix.Format = params.Format
|
|
}
|
|
|
|
// Ensure the format dir exists.
|
|
formatDir := filepath.Join(b.Config.Builder.ServerStateDir, "www", "version", "format"+b.State.Mix.Format)
|
|
err = os.MkdirAll(formatDir, 0777)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't create the format directory")
|
|
}
|
|
|
|
timer := &stopWatch{w: os.Stdout}
|
|
defer timer.WriteSummary(os.Stdout)
|
|
|
|
err = b.buildUpdateContent(params, timer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save upstream information.
|
|
if b.UpstreamURL != "" {
|
|
fmt.Printf("Saving the upstream URL: %s\n", b.UpstreamURL)
|
|
upstreamURLFile := filepath.Join(b.Config.Builder.ServerStateDir, "www", b.MixVer, "/upstreamurl")
|
|
err = ioutil.WriteFile(upstreamURLFile, []byte(b.UpstreamURL), 0644)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't write upstreamurl file")
|
|
}
|
|
fmt.Printf("Saving the upstream version: %s\n", b.UpstreamVer)
|
|
upstreamVerFile := filepath.Join(b.Config.Builder.ServerStateDir, "www", b.MixVer, "upstreamver")
|
|
err = ioutil.WriteFile(upstreamVerFile, []byte(b.UpstreamVer), 0644)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't write upstreamver file")
|
|
}
|
|
}
|
|
|
|
// Publish. Update the latest version both in the format (used by update itself) and in LAST_VER
|
|
// (used to create newer manifests).
|
|
if !params.Publish {
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("Setting latest version to %s\n", b.MixVer)
|
|
|
|
err = ioutil.WriteFile(filepath.Join(formatDir, "latest"), []byte(b.MixVer), 0644)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't update the latest version")
|
|
}
|
|
err = ioutil.WriteFile(filepath.Join(b.Config.Builder.ServerStateDir, "image", "LAST_VER"), []byte(b.MixVer), 0644)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't update the latest version")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Builder) buildUpdateContent(params UpdateParameters, timer *stopWatch) error {
|
|
var err error
|
|
|
|
// TODO: move this to parsing configuration / parameter time.
|
|
// TODO: should this be uint64?
|
|
var format uint32
|
|
format, err = parseUint32(b.State.Mix.Format)
|
|
if err != nil {
|
|
return errors.Errorf("invalid format")
|
|
}
|
|
|
|
minVersion := uint32(params.MinVersion)
|
|
|
|
err = writeMetaFiles(filepath.Join(b.Config.Builder.ServerStateDir, "www", b.MixVer), b.State.Mix.Format, Version)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to write update metadata files")
|
|
}
|
|
timer.Start("CREATE MANIFESTS")
|
|
mom, err := swupd.CreateManifests(b.MixVerUint32, minVersion, uint(format), b.Config.Builder.ServerStateDir, b.NumBundleWorkers)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to create update metadata")
|
|
}
|
|
fmt.Printf("MoM version %d\n", mom.Header.Version)
|
|
for _, f := range mom.Files {
|
|
fmt.Printf("- %-20s %d\n", f.Name, f.Version)
|
|
}
|
|
|
|
if !params.SkipSigning {
|
|
fmt.Println("Signing manifest.")
|
|
err = b.SignManifestMoM()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
outputDir := filepath.Join(b.Config.Builder.ServerStateDir, "www")
|
|
thisVersionDir := filepath.Join(outputDir, fmt.Sprint(b.MixVerUint32))
|
|
fmt.Println("Compressing Manifest.MoM")
|
|
momF := filepath.Join(thisVersionDir, "Manifest.MoM")
|
|
if params.SkipSigning {
|
|
err = createCompressedArchive(momF+".tar", momF)
|
|
} else {
|
|
err = createCompressedArchive(momF+".tar", momF, momF+".sig")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
// TODO: handle "too many open files" when mom.UpdatedBundles gets too
|
|
// large. May have to limit the number of workers here.
|
|
workers := len(mom.UpdatedBundles)
|
|
wg.Add(workers)
|
|
bundleChan := make(chan *swupd.Manifest)
|
|
errorChan := make(chan error, workers)
|
|
fmt.Println("Compressing bundle manifests")
|
|
compWorker := func() {
|
|
defer wg.Done()
|
|
for bundle := range bundleChan {
|
|
fmt.Printf(" %s\n", bundle.Name)
|
|
f := filepath.Join(thisVersionDir, "Manifest."+bundle.Name)
|
|
err = createCompressedArchive(f+".tar", f)
|
|
if err != nil {
|
|
errorChan <- err
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := 0; i < workers; i++ {
|
|
go compWorker()
|
|
}
|
|
|
|
for _, bundle := range mom.UpdatedBundles {
|
|
select {
|
|
case bundleChan <- bundle:
|
|
case err = <-errorChan:
|
|
// break as soon as we see a failure
|
|
break
|
|
}
|
|
}
|
|
close(bundleChan)
|
|
wg.Wait()
|
|
if err == nil && len(errorChan) > 0 {
|
|
err = <-errorChan
|
|
}
|
|
|
|
chanLen := len(errorChan)
|
|
for i := 0; i < chanLen; i++ {
|
|
<-errorChan
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Now tar the full manifest, since it doesn't show up in the MoM
|
|
fmt.Println(" full")
|
|
f := filepath.Join(thisVersionDir, "Manifest.full")
|
|
err = createCompressedArchive(f+".tar", f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Create manifest tars for Manifest.MoM and the mom.UpdatedBundles.
|
|
timer.Stop()
|
|
|
|
if !params.SkipFullfiles {
|
|
timer.Start("CREATE FULLFILES")
|
|
fmt.Printf("Using %d workers\n", b.NumFullfileWorkers)
|
|
fullfilesDir := filepath.Join(outputDir, b.MixVer, "files")
|
|
fullChrootDir := filepath.Join(b.Config.Builder.ServerStateDir, "image", b.MixVer, "full")
|
|
var info *swupd.FullfilesInfo
|
|
info, err = swupd.CreateFullfiles(mom.FullManifest, fullChrootDir, fullfilesDir, b.NumFullfileWorkers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Print summary of fullfile generation.
|
|
{
|
|
total := info.Skipped + info.NotCompressed
|
|
fmt.Printf("- Already created: %d\n", info.Skipped)
|
|
fmt.Printf("- Not compressed: %d\n", info.NotCompressed)
|
|
fmt.Printf("- Compressed\n")
|
|
for k, v := range info.CompressedCounts {
|
|
total += v
|
|
fmt.Printf(" - %-20s %d\n", k, v)
|
|
}
|
|
fmt.Printf("Total fullfiles: %d\n", total)
|
|
}
|
|
timer.Stop()
|
|
} else {
|
|
fmt.Println("\n=> CREATE FULLFILES - skipped")
|
|
}
|
|
|
|
if !params.SkipPacks {
|
|
timer.Start("CREATE ZERO PACKS")
|
|
bundleDir := filepath.Join(b.Config.Builder.ServerStateDir, "image")
|
|
for _, bundle := range mom.Files {
|
|
// TODO: Evaluate if it's worth using goroutines.
|
|
name := bundle.Name
|
|
version := bundle.Version
|
|
packPath := filepath.Join(outputDir, fmt.Sprint(version), swupd.GetPackFilename(name, 0))
|
|
_, err = os.Lstat(packPath)
|
|
if err == nil {
|
|
fmt.Printf("Zero pack already exists for %s to version %d\n", name, version)
|
|
continue
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "couldn't access existing pack file %s", packPath)
|
|
}
|
|
|
|
fmt.Printf("Creating zero pack for %s to version %d\n", name, version)
|
|
|
|
var info *swupd.PackInfo
|
|
info, err = swupd.CreatePack(name, 0, version, outputDir, bundleDir, 0)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't make pack for bundle %q", name)
|
|
}
|
|
if len(info.Warnings) > 0 {
|
|
fmt.Println("Warnings during pack:")
|
|
for _, w := range info.Warnings {
|
|
fmt.Printf(" %s\n", w)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
fmt.Printf(" Fullfiles in pack: %d\n", info.FullfileCount)
|
|
fmt.Printf(" Deltas in pack: %d\n", info.DeltaCount)
|
|
}
|
|
timer.Stop()
|
|
} else {
|
|
fmt.Println("\n=> CREATE ZERO PACKS - skipped")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// BuildImage will now proceed to build the full image with the previously
|
|
// validated configuration.
|
|
func (b *Builder) BuildImage(format string, template string) error {
|
|
// If the user did not pass in a format, default to builder.conf
|
|
if format == "" {
|
|
format = b.State.Mix.Format
|
|
}
|
|
|
|
// If the user did not pass in a template, default to release-image-config.json
|
|
if template == "" {
|
|
template = "release-image-config.json"
|
|
}
|
|
|
|
// swupd (client) called by itser will need a temporary directory to act as its stage dir.
|
|
wd, _ := os.Getwd()
|
|
tempStage, err := ioutil.TempDir(wd, "ister-swupd-client-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_ = os.RemoveAll(tempStage)
|
|
}()
|
|
|
|
content := "file://" + b.Config.Builder.ServerStateDir + "/www"
|
|
imagecmd := exec.Command("ister.py", "-S", tempStage, "-t", template, "-V", content, "-C", content, "-f", format, "-s", b.Config.Builder.Cert)
|
|
imagecmd.Stdout = os.Stdout
|
|
imagecmd.Stderr = os.Stderr
|
|
|
|
return imagecmd.Run()
|
|
}
|
|
|
|
type dnfRepoConf struct {
|
|
RepoName, RepoURL string
|
|
}
|
|
|
|
const dnfConfRepoTemplate = `
|
|
[{{.RepoName}}]
|
|
name={{.RepoName}}
|
|
failovermethod=priority
|
|
baseurl={{.RepoURL}}
|
|
enabled=1
|
|
gpgcheck=0
|
|
priority=1
|
|
`
|
|
|
|
// AddRepo adds and enables a repo configuration named <name> pointing at
|
|
// URL <url>. It calls b.NewDNFConfIfNeeded() to create the DNF config if it
|
|
// does not exist and performs a check to see if the repo passed has already
|
|
// been configured.
|
|
func (b *Builder) AddRepo(name, url string) error {
|
|
repo := dnfRepoConf{
|
|
RepoName: name,
|
|
RepoURL: url,
|
|
}
|
|
|
|
if err := b.NewDNFConfIfNeeded(); err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(b.Config.Builder.DNFConf, os.O_APPEND|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
helpers.PrintError(err)
|
|
return err
|
|
}
|
|
|
|
DNFConf, err := ini.Load(b.Config.Builder.DNFConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = DNFConf.GetSection(repo.RepoName)
|
|
if err == nil {
|
|
return fmt.Errorf("repo %s already exists in %s, not adding duplicate",
|
|
repo.RepoName,
|
|
b.Config.Builder.DNFConf)
|
|
}
|
|
|
|
t, err := textTemplate.New("dnfConfRepoTemplate").Parse(dnfConfRepoTemplate)
|
|
if err != nil {
|
|
helpers.PrintError(err)
|
|
return err
|
|
}
|
|
|
|
if err = t.Execute(f, repo); err != nil {
|
|
return errors.Wrapf(err, "Failed to write to dnf file: %s", b.Config.Builder.DNFConf)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetURLRepo sets the URL for the repo <name> to <url>. If <name> does not exist it is
|
|
// created.
|
|
func (b *Builder) SetURLRepo(name, url string) error {
|
|
if err := b.NewDNFConfIfNeeded(); err != nil {
|
|
return err
|
|
}
|
|
|
|
DNFConf, err := ini.Load(b.Config.Builder.DNFConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s, err := DNFConf.GetSection(name)
|
|
if err != nil {
|
|
// the section doesn't exist, just add a new one
|
|
return b.AddRepo(name, url)
|
|
}
|
|
|
|
k, err := s.GetKey("baseurl")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
k.SetValue(url)
|
|
return DNFConf.SaveTo(b.Config.Builder.DNFConf)
|
|
}
|
|
|
|
// SetExcludesRepo sets the ecludes for the repo <name> to [pkgs...]
|
|
func (b *Builder) SetExcludesRepo(reponame, pkgs string) error {
|
|
if err := b.NewDNFConfIfNeeded(); err != nil {
|
|
return err
|
|
}
|
|
|
|
DNFConf, err := ini.Load(b.Config.Builder.DNFConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s, err := DNFConf.GetSection(reponame)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
k, err := s.GetKey("excludepkgs")
|
|
if err != nil {
|
|
if k, err = DNFConf.Section(reponame).NewKey("excludepkgs", pkgs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
k.SetValue(pkgs)
|
|
return DNFConf.SaveTo(b.Config.Builder.DNFConf)
|
|
}
|
|
|
|
// RemoveRepo removes a configured repo <name> if it exists in the DNF configuration.
|
|
// This will fail if a DNF conf has not yet been generated.
|
|
func (b *Builder) RemoveRepo(name string) error {
|
|
if _, err := os.Stat(b.Config.Builder.DNFConf); os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
DNFConf, err := ini.Load(b.Config.Builder.DNFConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = DNFConf.GetSection(name)
|
|
if err != nil {
|
|
fmt.Printf("Repo %s does not exist.\n", name)
|
|
}
|
|
|
|
DNFConf.DeleteSection(name)
|
|
return DNFConf.SaveTo(b.Config.Builder.DNFConf)
|
|
}
|
|
|
|
// ListRepos lists all configured repositories in the DNF configuration file.
|
|
// This will fail if a DNF conf has not yet been generated.
|
|
func (b *Builder) ListRepos() error {
|
|
if _, err := os.Stat(b.Config.Builder.DNFConf); os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
DNFConf, err := ini.Load(b.Config.Builder.DNFConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, s := range DNFConf.Sections() {
|
|
name := s.Name()
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
url := s.Key("baseurl").Value()
|
|
if url == "" {
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("%s\t%s\n", name, url)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddRPMList copies rpms into the repodir and calls createrepo_c on it to
|
|
// generate a dnf-consumable repository for the bundle builder to use.
|
|
func (b *Builder) AddRPMList(rpms []string) error {
|
|
if b.Config.Mixer.LocalRepoDir == "" {
|
|
return errors.Errorf("LOCAL_REPO_DIR not set in configuration")
|
|
}
|
|
err := os.MkdirAll(b.Config.Mixer.LocalRepoDir, 0755)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't create LOCAL_REPO_DIR")
|
|
}
|
|
for _, rpm := range rpms {
|
|
localPath := filepath.Join(b.Config.Mixer.LocalRPMDir, rpm)
|
|
if err := checkRPM(localPath); err != nil {
|
|
return err
|
|
}
|
|
// Skip RPM already in repo.
|
|
repoPath := filepath.Join(b.Config.Mixer.LocalRepoDir, rpm)
|
|
if _, err := os.Stat(repoPath); err == nil {
|
|
continue
|
|
}
|
|
// Remove source RPMs because they should not be added to mixes
|
|
if strings.HasSuffix(rpm, ".src.rpm") {
|
|
fmt.Printf("Removing %s because source RPMs are not supported in mixes.\n", rpm)
|
|
if err := os.RemoveAll(filepath.Join(b.Config.Mixer.LocalRPMDir, rpm)); err != nil {
|
|
return errors.Wrapf(err, "Failed to remove %s, your mix will not generate properly with source RPMs included.", rpm)
|
|
}
|
|
}
|
|
fmt.Printf("Hardlinking %s to repodir\n", rpm)
|
|
if err := os.Link(localPath, repoPath); err != nil {
|
|
// Fallback to copying the file if hardlink fails.
|
|
err = helpers.CopyFile(repoPath, localPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("createrepo_c", ".")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Dir = b.Config.Mixer.LocalRepoDir
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
// checkRPM returns nil if path contains a valid RPM file.
|
|
func checkRPM(path string) error {
|
|
output, err := exec.Command("file", path).Output()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't check if %s is a RPM", path)
|
|
}
|
|
if !bytes.Contains(output, []byte("RPM v")) {
|
|
output = bytes.TrimSpace(output)
|
|
return errors.Errorf("file is not a RPM: %s", string(output))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseUint32(s string) (uint32, error) {
|
|
parsed, err := strconv.ParseUint(s, 10, 32)
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "error parsing value %q", s)
|
|
}
|
|
return uint32(parsed), nil
|
|
}
|
|
|
|
// createCompressedArchive will use tar and xz to create a compressed
|
|
// file. It does not stream the sources contents, doing all the work
|
|
// in memory before writing the destination file.
|
|
func createCompressedArchive(dst string, srcs ...string) error {
|
|
err := createCompressedArchiveInternal(dst, srcs...)
|
|
return errors.Wrapf(err, "couldn't create compressed archive %s", dst)
|
|
}
|
|
|
|
func createCompressedArchiveInternal(dst string, srcs ...string) error {
|
|
archive := &bytes.Buffer{}
|
|
xw, err := swupd.NewExternalWriter(archive, "xz")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = archiveFiles(xw, srcs)
|
|
if err != nil {
|
|
_ = xw.Close()
|
|
return err
|
|
}
|
|
|
|
err = xw.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ioutil.WriteFile(dst, archive.Bytes(), 0644)
|
|
}
|
|
|
|
func archiveFiles(w io.Writer, srcs []string) error {
|
|
tw := tar.NewWriter(w)
|
|
for _, src := range srcs {
|
|
fi, err := os.Lstat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !fi.Mode().IsRegular() {
|
|
return errors.Errorf("%s has unsupported type of file", src)
|
|
}
|
|
var hdr *tar.Header
|
|
hdr, err = tar.FileInfoHeader(fi, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = tw.WriteHeader(hdr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var srcData []byte
|
|
srcData, err = ioutil.ReadFile(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = tw.Write(srcData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tw.Close()
|
|
}
|
|
|
|
// BuildDeltaPacks between two versions of the mix.
|
|
func (b *Builder) BuildDeltaPacks(from, to uint32, printReport bool) error {
|
|
var err error
|
|
|
|
if to == 0 {
|
|
to = b.MixVerUint32
|
|
} else {
|
|
if to > b.MixVerUint32 {
|
|
return errors.Errorf("--to version must be at most the latest mix version (%d)", b.MixVerUint32)
|
|
}
|
|
}
|
|
if from == to {
|
|
fmt.Println("the --from version matches the --to version, nothing to do")
|
|
return nil
|
|
} else if from > to {
|
|
return errors.Errorf("the --from version must be smaller than the --to version")
|
|
}
|
|
|
|
outputDir := filepath.Join(b.Config.Builder.ServerStateDir, "www")
|
|
toManifest, err := swupd.ParseManifestFile(filepath.Join(outputDir, fmt.Sprint(to), "Manifest.MoM"))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't find manifest of target version")
|
|
}
|
|
|
|
fromManifest, err := swupd.ParseManifestFile(filepath.Join(outputDir, fmt.Sprint(from), "Manifest.MoM"))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't find manifest of from version")
|
|
}
|
|
|
|
bundleDir := filepath.Join(b.Config.Builder.ServerStateDir, "image")
|
|
fmt.Printf("Using %d workers\n", b.NumDeltaWorkers)
|
|
// Create all deltas first
|
|
err = swupd.CreateAllDeltas(outputDir, int(fromManifest.Header.Version), int(toManifest.Header.Version), b.NumDeltaWorkers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Create packs filling in any missing deltas
|
|
return createDeltaPacks(fromManifest, toManifest, printReport, outputDir, bundleDir, b.NumDeltaWorkers)
|
|
}
|
|
|
|
// BuildDeltaPacksPreviousVersions builds packs to version from up to
|
|
// prev versions. It walks the Manifest "previous" field to find those from versions.
|
|
func (b *Builder) BuildDeltaPacksPreviousVersions(prev, to uint32, printReport bool) error {
|
|
var err error
|
|
|
|
if to == 0 {
|
|
to = b.MixVerUint32
|
|
} else {
|
|
if to > b.MixVerUint32 {
|
|
return errors.Errorf("--to version must be at most the latest mix version (%d)", b.MixVerUint32)
|
|
}
|
|
}
|
|
|
|
outputDir := filepath.Join(b.Config.Builder.ServerStateDir, "www")
|
|
toManifest, err := swupd.ParseManifestFile(filepath.Join(outputDir, fmt.Sprint(to), "Manifest.MoM"))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't find manifest of target version")
|
|
}
|
|
|
|
var previousManifests []*swupd.Manifest
|
|
cur := toManifest.Header.Previous
|
|
for i := uint32(0); i < prev; i++ {
|
|
if cur == 0 {
|
|
break
|
|
}
|
|
var m *swupd.Manifest
|
|
m, err = swupd.ParseManifestFile(filepath.Join(outputDir, fmt.Sprint(cur), "Manifest.MoM"))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "could not find manifest for previous version %d, skipping...\n", cur)
|
|
continue
|
|
}
|
|
previousManifests = append(previousManifests, m)
|
|
cur = m.Header.Previous
|
|
}
|
|
|
|
fmt.Printf("Found %d previous versions\n", len(previousManifests))
|
|
if len(previousManifests) == 0 {
|
|
return nil
|
|
}
|
|
bundleDir := filepath.Join(b.Config.Builder.ServerStateDir, "image")
|
|
// Create all deltas for all previous versions first based on full manifests
|
|
var versionQueue = make(chan *swupd.Manifest)
|
|
var wg sync.WaitGroup
|
|
var deltaErrors []error
|
|
versionWorkers := 1
|
|
|
|
// If we have at least 2x the number of CPUs as versions, give each version
|
|
// its own thread to build deltas in.
|
|
if runtime.NumCPU() >= 2*len(previousManifests) {
|
|
b.NumDeltaWorkers = int(math.Ceil(float64(runtime.NumCPU()) / float64(len(previousManifests))))
|
|
versionWorkers = len(previousManifests)
|
|
}
|
|
wg.Add(versionWorkers)
|
|
fmt.Printf("Using %d version threads and %d delta threads in each\n", versionWorkers, b.NumDeltaWorkers)
|
|
|
|
// If possible, run a thread for each version back so we don't get locked up
|
|
// at the end of a version doing some large/slow delta pack in serial. This way
|
|
// the large file(s) at the end of each version will run in parallel.
|
|
for i := 0; i < versionWorkers; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for fromManifest := range versionQueue {
|
|
deltaErr := swupd.CreateAllDeltas(outputDir, int(fromManifest.Header.Version), int(toManifest.Header.Version), b.NumDeltaWorkers)
|
|
if deltaErr != nil {
|
|
deltaErrors = append(deltaErrors, deltaErr)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Send jobs to the queue for version goroutines to pick up.
|
|
for i := range previousManifests {
|
|
versionQueue <- previousManifests[i]
|
|
}
|
|
|
|
// Send message that no more jobs are being sent
|
|
close(versionQueue)
|
|
wg.Wait()
|
|
|
|
for i := 0; i < len(deltaErrors); i++ {
|
|
fmt.Fprintf(os.Stderr, "%s\n", deltaErrors[i])
|
|
}
|
|
|
|
// Simply pack all deltas up since they are now created
|
|
for _, fromManifest := range previousManifests {
|
|
fmt.Println()
|
|
err = createDeltaPacks(fromManifest, toManifest, printReport, outputDir, bundleDir, b.NumDeltaWorkers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createDeltaPacks(fromMoM *swupd.Manifest, toMoM *swupd.Manifest, printReport bool, outputDir, bundleDir string, numWorkers int) error {
|
|
timer := &stopWatch{w: os.Stdout}
|
|
defer timer.WriteSummary(os.Stdout)
|
|
timer.Start("CREATE DELTA PACKS")
|
|
|
|
fmt.Printf("Creating delta packs from %d to %d\n", fromMoM.Header.Version, toMoM.Header.Version)
|
|
bundlesToPack, err := swupd.FindBundlesToPack(fromMoM, toMoM)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get an ordered output. This make easy to compare different runs.
|
|
var orderedBundles []string
|
|
for name := range bundlesToPack {
|
|
orderedBundles = append(orderedBundles, name)
|
|
}
|
|
sort.Strings(orderedBundles)
|
|
|
|
for _, name := range orderedBundles {
|
|
b := bundlesToPack[name]
|
|
packPath := filepath.Join(outputDir, fmt.Sprint(b.ToVersion), swupd.GetPackFilename(b.Name, b.FromVersion))
|
|
_, err = os.Lstat(packPath)
|
|
if err == nil {
|
|
fmt.Printf(" Delta pack already exists for %s from %d to %d\n", b.Name, b.FromVersion, b.ToVersion)
|
|
// Remove so the goroutines don't try to make deltas for these
|
|
delete(bundlesToPack, name)
|
|
continue
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "couldn't access existing pack file %s", packPath)
|
|
}
|
|
}
|
|
|
|
if numWorkers < 1 {
|
|
numWorkers = runtime.NumCPU()
|
|
}
|
|
bundleWorkers := numWorkers
|
|
|
|
var bundleQueue = make(chan *swupd.BundleToPack)
|
|
var wg sync.WaitGroup
|
|
wg.Add(bundleWorkers)
|
|
|
|
// Delta creation takes a lot of memory, so create a limited amount of goroutines.
|
|
for i := 0; i < bundleWorkers; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for b := range bundleQueue {
|
|
fmt.Printf(" Creating delta pack for bundle %q from %d to %d\n", b.Name, b.FromVersion, b.ToVersion)
|
|
info, err := swupd.CreatePack(b.Name, b.FromVersion, b.ToVersion, outputDir, bundleDir, numWorkers)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR: Pack %q from %d to %d FAILED to be created: %s\n", b.Name, b.FromVersion, b.ToVersion, err)
|
|
// Do not exit on errors, we have logging for all other failures and deltas are optional
|
|
continue
|
|
}
|
|
|
|
if len(info.Warnings) > 0 {
|
|
for _, w := range info.Warnings {
|
|
fmt.Printf(" WARNING: %s\n", w)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
if printReport {
|
|
max := 0
|
|
for _, e := range info.Entries {
|
|
if len(e.File.Name) > max {
|
|
max = len(e.File.Name)
|
|
}
|
|
}
|
|
fmt.Println(" Pack report:")
|
|
for _, e := range info.Entries {
|
|
fmt.Printf(" %-*s %s (%s)\n", max, e.File.Name, e.State, e.Reason)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
fmt.Printf(" Fullfiles in pack: %d\n", info.FullfileCount)
|
|
fmt.Printf(" Deltas in pack: %d\n", info.DeltaCount)
|
|
}
|
|
}()
|
|
}
|
|
// Send jobs to the queue for delta goroutines to pick up.
|
|
for _, bundle := range bundlesToPack {
|
|
bundleQueue <- bundle
|
|
}
|
|
// Send message that no more jobs are being sent
|
|
close(bundleQueue)
|
|
wg.Wait()
|
|
|
|
timer.Stop()
|
|
return nil
|
|
}
|
|
|
|
// writeMetaFiles writes mixer and format metadata to files
|
|
func writeMetaFiles(path, format, version string) error {
|
|
err := os.MkdirAll(path, 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ioutil.WriteFile(filepath.Join(path, "format"), []byte(format), 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ioutil.WriteFile(filepath.Join(path, "mixer-src-version"), []byte(version), 0644)
|
|
}
|
|
|
|
func (b *Builder) getUpstreamFormat(version string) (string, error) {
|
|
format, err := b.DownloadFileFromUpstreamAsString(fmt.Sprintf("update/%s/format", version))
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "Failed to get format for version %q", version)
|
|
}
|
|
return format, nil
|
|
}
|
|
|
|
func (b *Builder) getUpstreamFormatRange(version string) (format string, first, latest uint32, err error) {
|
|
format, err = b.getUpstreamFormat(version)
|
|
if err != nil {
|
|
return "", 0, 0, errors.Wrap(err, "couldn't download information about upstream")
|
|
}
|
|
|
|
readUint32 := func(subpath string) (uint32, error) {
|
|
str, rerr := b.DownloadFileFromUpstreamAsString(subpath)
|
|
if rerr != nil {
|
|
return 0, rerr
|
|
}
|
|
val, rerr := parseUint32(str)
|
|
if rerr != nil {
|
|
return 0, rerr
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
latest, err = readUint32(fmt.Sprintf("update/version/format%s/latest", format))
|
|
if err != nil {
|
|
return "", 0, 0, errors.Wrap(err, "couldn't read information about upstream")
|
|
}
|
|
|
|
// TODO: Clear Linux does produce the "first" files, but not sure mixes got
|
|
// those. We should add those (or change this to walk previous format latest).
|
|
first, err = readUint32(fmt.Sprintf("update/version/format%s/first", format))
|
|
if err != nil {
|
|
return "", 0, 0, errors.Wrap(err, "couldn't read information about upstream")
|
|
}
|
|
|
|
return format, first, latest, err
|
|
}
|
|
|
|
// PrintVersions prints the current mix and upstream versions, and the
|
|
// latest version of upstream.
|
|
func (b *Builder) PrintVersions() error {
|
|
format, first, latest, err := b.getUpstreamFormatRange(b.UpstreamVer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf(`
|
|
Current mix: %d
|
|
Current upstream: %d (format: %s)
|
|
|
|
First upstream in format: %d
|
|
Latest upstream in format: %d
|
|
`, b.MixVerUint32, b.UpstreamVerUint32, format, first, latest)
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateVersions will validate then update both mix and upstream versions. If
|
|
// upstream version is 0, then the latest upstream version in the current
|
|
// upstream format will be taken instead.
|
|
func (b *Builder) UpdateVersions(nextMix, nextUpstream uint32) error {
|
|
format, _, latest, err := b.getUpstreamFormatRange(b.UpstreamVer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if nextMix <= b.MixVerUint32 {
|
|
return fmt.Errorf("invalid mix version to update (%d), need to be greater than current mix version (%d)", nextMix, b.MixVerUint32)
|
|
}
|
|
|
|
if nextUpstream == 0 {
|
|
nextUpstream = latest
|
|
}
|
|
|
|
nextUpstreamStr := strconv.FormatUint(uint64(nextUpstream), 10)
|
|
|
|
nextFormat := format
|
|
if nextUpstream > latest {
|
|
nextFormat, err = b.getUpstreamFormat(nextUpstreamStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Verify the version exists by checking if its Manifest.MoM is around.
|
|
_, err = b.DownloadFileFromUpstreamAsString(fmt.Sprintf("/update/%d/Manifest.MoM", nextUpstream))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "invalid upstream version %d", nextUpstream)
|
|
}
|
|
|
|
fmt.Printf(`Old mix: %d
|
|
Old upstream: %d (format: %s)
|
|
|
|
New mix: %d
|
|
New upstream: %d (format: %s)
|
|
`, b.MixVerUint32, b.UpstreamVerUint32, format, nextMix, nextUpstream, nextFormat)
|
|
|
|
mixVerContents := []byte(fmt.Sprintf("%d\n", nextMix))
|
|
err = ioutil.WriteFile(filepath.Join(b.Config.Builder.VersionPath, b.MixVerFile), mixVerContents, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "couldn't write updated mix version")
|
|
}
|
|
fmt.Printf("\nWrote %s.\n", b.MixVerFile)
|
|
|
|
upstreamVerContents := []byte(nextUpstreamStr + "\n")
|
|
err = ioutil.WriteFile(filepath.Join(b.Config.Builder.VersionPath, b.UpstreamVerFile), upstreamVerContents, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "couldn't write updated upstream version")
|
|
}
|
|
fmt.Printf("Wrote %s.\n", b.UpstreamVerFile)
|
|
b.UpstreamVerUint32 = nextUpstream
|
|
b.UpstreamVer = nextUpstreamStr
|
|
|
|
if _, err := b.CheckBumpNeeded(false); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|