Files
mixer-tools/builder/builder.go
T
Rodrigo Chiossi f697373abd builder: Fix URL build for base URL with subpath
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>
2018-09-21 23:15:55 +02:00

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
}