mirror of
https://github.com/clearlinux/mixer-tools.git
synced 2026-09-03 20:21:32 +00:00
800 lines
26 KiB
Go
800 lines
26 KiB
Go
// Copyright © 2018 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 (
|
|
"bytes"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/clearlinux/mixer-tools/config"
|
|
"github.com/clearlinux/mixer-tools/helpers"
|
|
"github.com/clearlinux/mixer-tools/log"
|
|
"github.com/clearlinux/mixer-tools/swupd"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Version of Mixer. This is provided by ldflags in Makefile during compilation
|
|
var Version = ""
|
|
|
|
// 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
|
|
repos map[string]repoInfo
|
|
}
|
|
|
|
// 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
|
|
// 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(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := b.Config.LoadConfig(conf); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := b.State.Load(b.Config); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := b.ReadVersions(); err != nil {
|
|
return nil, err
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// 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"
|
|
log.Warning(log.Mixer, "'.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
|
|
}
|
|
|
|
log.Info(log.Mixer, "Initializing mix version %s from upstream version %s", mixVer, upstreamVer)
|
|
|
|
// Deprecate '.clearversion' --> 'upstreamversion'
|
|
if _, err := os.Stat(filepath.Join(b.Config.Builder.VersionPath, ".clearversion")); err == nil {
|
|
b.UpstreamVerFile = ".clearversion"
|
|
log.Warning(log.Mixer, "'.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"
|
|
log.Warning(log.Mixer, "'.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 {
|
|
log.Info(log.Mixer, "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"}
|
|
}
|
|
|
|
// Clean up the upstream bundles folder and its contents
|
|
if err := os.RemoveAll(upstreamBundlesBaseDir); err != nil {
|
|
return errors.Wrap(err, "Failed to delete upstream-bundles dir")
|
|
}
|
|
|
|
if err := b.AddBundles(bundles, allLocal, allUpstream, false); 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
|
|
}
|
|
}
|
|
|
|
// Create the DNF conf early in case we want to edit before building a first mix
|
|
if err := b.NewDNFConfIfNeeded(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return 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 bool, downloadRetries int) error {
|
|
// Fetch upstream bundle files if needed
|
|
if err := b.getUpstreamBundles(); 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
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Clean existing image and output directories
|
|
mixImageDir := path.Join(b.Config.Builder.ServerStateDir, "image", b.MixVer)
|
|
if _, err := os.Stat(mixImageDir); err == nil {
|
|
log.Warning(log.Mixer, "Wiping away existing image directory for mix version %s...", b.MixVer)
|
|
if err = os.RemoveAll(mixImageDir); err != nil {
|
|
return err
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
mixOutputDir := path.Join(b.Config.Builder.ServerStateDir, "www", b.MixVer)
|
|
if _, err := os.Stat(mixOutputDir); err == nil {
|
|
log.Warning(log.Mixer, "Wiping away existing output directory for mix version %s...", b.MixVer)
|
|
if err = os.RemoveAll(mixOutputDir); err != nil {
|
|
return err
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
// Generate the certificate needed for signing verification if it does not exist
|
|
if !signflag && 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, downloadRetries)
|
|
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 != "" {
|
|
log.Info(log.Mixer, "Saving the upstream URL: %s", 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")
|
|
}
|
|
log.Info(log.Mixer, "Saving the upstream version: %s", 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")
|
|
}
|
|
}
|
|
|
|
log.Info(log.Mixer, "Setting latest version to %s", b.MixVer)
|
|
|
|
latestVerFilePath := filepath.Join(b.Config.Builder.ServerStateDir, "www", "version", "latest_version")
|
|
err = ioutil.WriteFile(latestVerFilePath, []byte(b.MixVer), 0644)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't update the latest_version file")
|
|
}
|
|
|
|
// sign the latest_version file
|
|
if !params.SkipSigning {
|
|
log.Info(log.Mixer, "Signing latest_version file")
|
|
err = b.signFile(latestVerFilePath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't sign the latest_version file")
|
|
}
|
|
}
|
|
err = ioutil.WriteFile(filepath.Join(formatDir, "latest"), []byte(b.MixVer), 0644)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't update the latest version")
|
|
}
|
|
|
|
// sign the latest file in place based on the Mixed format
|
|
// read from builder.conf.
|
|
if !params.SkipSigning {
|
|
log.Info(log.Mixer, "Signing latest file")
|
|
err = b.signFile(filepath.Join(formatDir, "latest"))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "couldn't sign the latest file")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
const migrationConfig = "release-image-config.json"
|
|
const buildConfig = "release-image-config.yaml"
|
|
|
|
// BuildImage will now proceed to build the full image with the previously
|
|
// validated configuration.
|
|
func (b *Builder) BuildImage(format string, configFile 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 configFile, use the default configFile
|
|
if configFile == "" {
|
|
configFile = filepath.Join(b.Config.Builder.VersionPath, buildConfig)
|
|
migrationFile := filepath.Join(b.Config.Builder.VersionPath, migrationConfig)
|
|
|
|
// If the default configFile is not present in the mix workspace
|
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
|
// If old default JSON !exists
|
|
if _, err := os.Stat(migrationFile); os.IsNotExist(err) {
|
|
// create default YAML with clr-installer tool updating the bundle list based on mix bundles
|
|
log.Warning(log.Mixer, "Image configuration file %s not found; generating", buildConfig)
|
|
if err = b.generateImageConfig(configFile); err != nil {
|
|
return err
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
} else {
|
|
// The previous ister.py configuration file exist,
|
|
// we need to migrate it to clr-installer YAML
|
|
log.Warning(log.Mixer, "Previous generation image config %s found", migrationFile)
|
|
if err = b.migrateConfigFile(migrationFile); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the legacy JSON file exists, rename the old file to prevent migration
|
|
// each time we build and discourage the user from using the old JSON file.
|
|
if _, err := os.Stat(migrationFile); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
renameFile := migrationFile + "-EOL"
|
|
log.Info(log.Mixer, " Renaming previous generation config %s to %s", migrationFile, renameFile)
|
|
if mvErr := os.Rename(migrationFile, renameFile); mvErr != nil {
|
|
log.Warning(log.Mixer, "Failed to rename %s: %v", migrationFile, mvErr)
|
|
}
|
|
|
|
} else {
|
|
if !strings.HasSuffix(configFile, "yaml") {
|
|
return fmt.Errorf("build configuration file '%s' must end in .yaml", configFile)
|
|
}
|
|
}
|
|
log.Info(log.Mixer, "Building image, please wait...")
|
|
clrLogFile := strings.Replace(configFile, ".yaml", ".log", 1)
|
|
content := "file://" + b.Config.Builder.ServerStateDir + "/www"
|
|
imagecmd := exec.Command("clr-installer", "-c", configFile, "--swupd-versionurl",
|
|
content, "--swupd-contenturl", content, "--swupd-format", format,
|
|
"--swupd-cert", b.Config.Builder.Cert, "--log-file", clrLogFile)
|
|
var out bytes.Buffer
|
|
var errBuf bytes.Buffer
|
|
imagecmd.Stdout = &out
|
|
imagecmd.Stderr = &errBuf
|
|
err := imagecmd.Run()
|
|
if err != nil {
|
|
log.Error(log.Installer, errBuf.String()+err.Error())
|
|
log.Debug(log.Installer, out.String())
|
|
return errors.New("failed to create image")
|
|
}
|
|
log.Verbose(log.Installer, out.String())
|
|
log.Info(log.Mixer, "Image built successfully!")
|
|
return err
|
|
}
|
|
|
|
// migrateConfigFile will generate a clr-install YAML file based on the existing
|
|
// ister JSON file. We will also rename the existing ister JSON to prevent it
|
|
// from being migrated a second time.
|
|
func (b *Builder) migrateConfigFile(migrationFile string) error {
|
|
log.Info(log.Mixer, "Migrating image config from %s to %s", migrationConfig, buildConfig)
|
|
|
|
convertCmd := exec.Command("clr-installer", "--json-yaml", migrationFile, "--iso", "--keep-image")
|
|
var out bytes.Buffer
|
|
var errBuf bytes.Buffer
|
|
convertCmd.Stdout = &out
|
|
convertCmd.Stderr = &errBuf
|
|
err := convertCmd.Run()
|
|
if err != nil {
|
|
log.Error(log.Installer, errBuf.String()+err.Error())
|
|
log.Debug(log.Installer, out.String())
|
|
return errors.New("failed to migrate config file")
|
|
}
|
|
log.Verbose(log.Installer, out.String())
|
|
return err
|
|
}
|
|
|
|
// generateImageConfig will create the image template using the clr-installer tool
|
|
// and update the image bundle list based on mix bundles.
|
|
// If there is an error updating the image bundle list, the default bundle list will be used.
|
|
func (b *Builder) generateImageConfig(configFile string) error {
|
|
log.Info(log.Mixer, "Updating image bundle list based on %s", b.MixBundlesFile)
|
|
|
|
var cmdOpts []string
|
|
var bundles []string
|
|
|
|
cmdOpts = append(cmdOpts, "--template", configFile) // set the output filename
|
|
cmdOpts = append(cmdOpts, "--iso") // enable ISO generation
|
|
cmdOpts = append(cmdOpts, "--keep-image") // keep the raw image file too
|
|
mixBundles, err := b.getMixBundlesListAsSet() // returns empty set with no error if mix bundles file is not present
|
|
if err == nil && len(mixBundles) > 0 { // check if set is not empty
|
|
for _, bundle := range mixBundles {
|
|
bundles = append(bundles, bundle.Name)
|
|
}
|
|
cmdOpts = append(cmdOpts, "--bundles", strings.Join(bundles, ",")) // Add mix bundles
|
|
} else {
|
|
log.Warning(log.Mixer, "Failed to read %s. Using default bundle list instead", b.MixBundlesFile)
|
|
}
|
|
|
|
convertCmd := exec.Command("clr-installer", cmdOpts...)
|
|
var out bytes.Buffer
|
|
var errBuf bytes.Buffer
|
|
|
|
convertCmd.Stdout = &out
|
|
convertCmd.Stderr = &errBuf
|
|
err = convertCmd.Run()
|
|
if err != nil {
|
|
log.Error(log.Installer, errBuf.String()+err.Error())
|
|
log.Debug(log.Installer, out.String())
|
|
return errors.New("failed to generate image config")
|
|
}
|
|
log.Verbose(log.Installer, out.String())
|
|
return err
|
|
}
|
|
|
|
// 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 {
|
|
log.Info(log.Mixer, "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")
|
|
// 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 {
|
|
log.Warning(log.Mixer, "Could not find manifest for previous version %d, skipping...", cur)
|
|
continue
|
|
}
|
|
// do not create delta-packs over format bumps since clients can't update
|
|
// past the boundary anyways. Only check for inequality, if the format
|
|
// goes down that should be checked elsewhere.
|
|
if m.Header.Format != toManifest.Header.Format {
|
|
log.Warning(log.Mixer, "Skipping delta-pack creation over format bump")
|
|
continue
|
|
}
|
|
previousManifests = append(previousManifests, m)
|
|
cur = m.Header.Previous
|
|
}
|
|
|
|
log.Info(log.Mixer, "Found %d previous versions", 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)
|
|
mux := &sync.Mutex{}
|
|
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)
|
|
log.Info(log.Mixer, "Using %d version threads and %d delta threads in each", 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 {
|
|
mux.Lock()
|
|
deltaErrors = append(deltaErrors, deltaErr)
|
|
mux.Unlock()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// 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++ {
|
|
log.Info(log.Mixer, "%s", deltaErrors[i])
|
|
}
|
|
|
|
// Simply pack all deltas up since they are now created
|
|
for _, fromManifest := range previousManifests {
|
|
err = createDeltaPacks(fromManifest, toManifest, printReport, outputDir, bundleDir, b.NumDeltaWorkers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildDeltaManifests between two versions of the mix.
|
|
func (b *Builder) BuildDeltaManifests(from, to uint32) 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 {
|
|
log.Info(log.Mixer, "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")
|
|
}
|
|
|
|
log.Info(log.Mixer, "Using %d workers", b.NumDeltaWorkers)
|
|
log.Info(log.Mixer, "Creating Manifest delta files from %d to %d", from, to)
|
|
deltas, err := swupd.CreateManifestDeltas(b.Config.Builder.ServerStateDir, fromManifest, toManifest, b.NumDeltaWorkers)
|
|
if err != nil {
|
|
log.Info(log.Mixer, " %s", err)
|
|
} else {
|
|
created := 0
|
|
for _, delta := range deltas {
|
|
if delta.Error == nil {
|
|
created++
|
|
}
|
|
}
|
|
log.Info(log.Mixer, " Created %d Manifest delta files", created)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildDeltaManifestsPreviousVersions builds manifests to version from up to
|
|
// prev versions. It walks the Manifest "previous" field to find those from versions.
|
|
func (b *Builder) BuildDeltaManifestsPreviousVersions(prev, to uint32) 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 {
|
|
log.Warning(log.Mixer, "Could not find manifest for previous version %d, skipping...", cur)
|
|
continue
|
|
}
|
|
// do not create delta-manifests over format bumps since clients can't update
|
|
// past the boundary anyways. Only check for inequality, if the format
|
|
// goes down that should be checked elsewhere.
|
|
if m.Header.Format != toManifest.Header.Format {
|
|
log.Warning(log.Mixer, "Skipping delta-pack creation over format bump")
|
|
break
|
|
}
|
|
previousManifests = append(previousManifests, m)
|
|
cur = m.Header.Previous
|
|
}
|
|
|
|
log.Info(log.Mixer, "Found %d previous versions", len(previousManifests))
|
|
if len(previousManifests) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, i := range previousManifests {
|
|
log.Info(log.Mixer, "Creating Manifest delta files from %d to %d", i.Header.Version, to)
|
|
deltas, deltaErr := swupd.CreateManifestDeltas(b.Config.Builder.ServerStateDir, i, toManifest, b.NumDeltaWorkers)
|
|
if deltaErr != nil {
|
|
log.Warning(log.Mixer, " %s", err)
|
|
} else {
|
|
created := 0
|
|
for _, delta := range deltas {
|
|
if delta.Error == nil {
|
|
created++
|
|
}
|
|
}
|
|
log.Info(log.Mixer, " Created %d Manifest delta files", created)
|
|
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|