mirror of
https://github.com/clearlinux/mixer-tools.git
synced 2026-09-04 04:31:44 +00:00
Implement the logging package for mixer. A common log file can be set for all the mixer commands in the builder.conf. E.g. `mixer config set Mixer.LOG <filepath>` The log file and level can also be set for individual mixer commands using the `--log` and `--log-level` flags respectively. The various log levels are: ERROR (1), WARNING (2), INFO (3), DEBUG (4) and VERBOSE (5). Default log level is 4. Fixes #666 Signed-off-by: Ashlesha Atrey ashlesha.atrey@intel.com
1350 lines
39 KiB
Go
1350 lines
39 KiB
Go
package builder
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/clearlinux/mixer-tools/helpers"
|
|
"github.com/clearlinux/mixer-tools/log"
|
|
"github.com/clearlinux/mixer-tools/swupd"
|
|
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type bundleStatus int
|
|
|
|
// MinMcaTableWidth is the minimum width for MCA statistics table
|
|
const MinMcaTableWidth = 80
|
|
|
|
const (
|
|
unchanged bundleStatus = iota
|
|
added
|
|
removed
|
|
modified
|
|
)
|
|
|
|
// Contains all results from MCA diff and lists of added/deleted/modified bundles
|
|
type mcaDiffResults struct {
|
|
bundleDiff []*mcaBundleDiff
|
|
|
|
addList []string
|
|
delList []string
|
|
modList []string
|
|
}
|
|
|
|
// MCA diff results for a bundle
|
|
type mcaBundleDiff struct {
|
|
name string
|
|
status bundleStatus
|
|
minversion bool
|
|
pkgFileCounts map[string]*bundlePkgStats
|
|
|
|
pkgFileDiffs diffLists
|
|
manFileDiffs diffLists
|
|
pkgDiffs diffLists
|
|
}
|
|
|
|
// File counters for a package
|
|
type bundlePkgStats struct {
|
|
add int
|
|
del int
|
|
mod int
|
|
}
|
|
|
|
// Lists of added, deleted, and modified packages or files
|
|
type diffLists struct {
|
|
addList []string
|
|
delList []string
|
|
modList []string
|
|
}
|
|
|
|
// mcaBundleInfo contains manifest and package metadata necessary to perform an
|
|
// MCA diff on a bundle.
|
|
type mcaBundleInfo struct {
|
|
size uint64
|
|
|
|
subPkgs map[string]bool
|
|
subPkgFiles map[string]*fileInfo
|
|
manFiles map[string]*swupd.File
|
|
}
|
|
|
|
// mcaBundlePkgInfo contains MCA package and resolved file lists for a bundle
|
|
// before subtraction.
|
|
type mcaBundlePkgInfo struct {
|
|
name string
|
|
allPkgs map[string]*pkgInfo
|
|
allFiles map[string]bool
|
|
}
|
|
|
|
// pkgInfo contains package metadata
|
|
type pkgInfo struct {
|
|
name string
|
|
uri string
|
|
|
|
files []*fileInfo
|
|
}
|
|
|
|
// fileInfo contains file metadata
|
|
type fileInfo struct {
|
|
name string
|
|
size string
|
|
hash string
|
|
modes string
|
|
links string
|
|
user string
|
|
group string
|
|
|
|
hashLen int
|
|
pkg string
|
|
}
|
|
|
|
// CheckManifestCorrectness validates that the changes in manifest files between
|
|
// two versions aligns to the corresponding RPM changes. Any mismatched files
|
|
// between the manifests and RPMs will be printed as errors. When there are no
|
|
// errors, package and file statistics for each modified bundle will be displayed.
|
|
func (b *Builder) CheckManifestCorrectness(fromVer, toVer, downloadRetries, tableWidth int, fromRepoURLOverrides, toRepoURLOverrides map[string]string) error {
|
|
if fromVer < 0 || toVer < 0 {
|
|
return fmt.Errorf("Negative version not supported")
|
|
}
|
|
|
|
if fromVer >= toVer {
|
|
return fmt.Errorf("From version must be less than to version")
|
|
}
|
|
|
|
// Suppress Stdout so that it doesn't clutter the results
|
|
stdOut := os.Stdout
|
|
os.Stdout, _ = os.Open(os.DevNull)
|
|
defer func() {
|
|
os.Stdout = stdOut
|
|
}()
|
|
|
|
if err := b.NewDNFConfIfNeeded(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load initial repo map
|
|
if err := b.ListRepos(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get manifest file lists and subtracted RPM pkg/file lists
|
|
fromInfo, err := b.mcaInfo(fromVer, downloadRetries, fromRepoURLOverrides)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
toInfo, err := b.mcaInfo(toVer, downloadRetries, toRepoURLOverrides)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Diff from version's manifest file list and subtracted RPM pkg/file lists
|
|
// against the to version's lists
|
|
results, err := diffMcaInfo(fromInfo, toInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Compare the manifest file changes against the RPM file changes to determine
|
|
// any errors.
|
|
errorList, warningList, err := analyzeMcaResults(b, results, fromInfo, toInfo, fromVer, toVer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Re-enable Stdout for the results
|
|
os.Stdout = stdOut
|
|
|
|
// Display errors and package/file statistics
|
|
err = printMcaResults(results, fromInfo, toInfo, fromVer, toVer, tableWidth, errorList, warningList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// mcaInfo collects manifest/RPM metadata and uses it to create a list
|
|
// of manifest files, subtracted packages, and subtracted package files for each
|
|
// bundle in the provided version.
|
|
func (b *Builder) mcaInfo(version, downloadRetries int, repoURLOverrides map[string]string) (map[string]*mcaBundleInfo, error) {
|
|
allBundleInfo := make(map[string]*mcaBundleInfo)
|
|
|
|
// Get manifest info for valid bundle entries in the MoM.
|
|
mInfo, err := b.mcaManInfo(version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Download and collect metadata for all packages
|
|
pInfo, err := b.mcaPkgInfo(mInfo, version, downloadRetries, repoURLOverrides)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get manifest file lists, subtracted package lists, and subtracted package file lists
|
|
for _, m := range mInfo {
|
|
info := &mcaBundleInfo{
|
|
manFiles: make(map[string]*swupd.File),
|
|
subPkgFiles: make(map[string]*fileInfo),
|
|
subPkgs: make(map[string]bool),
|
|
size: m.Header.ContentSize,
|
|
}
|
|
|
|
err = info.getManFiles(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = info.getSubPkgs(m, pInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = info.getSubPkgFiles(m, pInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allBundleInfo[m.Name] = info
|
|
}
|
|
return allBundleInfo, nil
|
|
}
|
|
|
|
// mcaManInfo collects manifest metadata for each bundle listed in
|
|
// the provided version's MoM.
|
|
func (b *Builder) mcaManInfo(version int) ([]*swupd.Manifest, error) {
|
|
manifests := []*swupd.Manifest{}
|
|
|
|
updateDir := filepath.Join(b.Config.Builder.ServerStateDir, "www")
|
|
versionDir := filepath.Join(updateDir, fmt.Sprint(version))
|
|
momPath := filepath.Join(versionDir, "Manifest.MoM")
|
|
|
|
mom, err := swupd.ParseManifestFile(momPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get bundle info for each MoM entry
|
|
for _, f := range mom.Files {
|
|
// os-core-update-index and iterative manifests are not checked by MCA
|
|
if f.Name == "os-core-update-index" {
|
|
continue
|
|
}
|
|
|
|
manifestPath := filepath.Join(updateDir, fmt.Sprint(f.Version), "Manifest."+f.Name)
|
|
manifest, err := swupd.ParseManifestFile(manifestPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bundleInfoPath := filepath.Join(b.Config.Builder.ServerStateDir, "image", fmt.Sprint(version), manifest.Name+"-info")
|
|
err = manifest.GetBundleInfo(b.Config.Builder.ServerStateDir, bundleInfoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// skip bundle if bundleInfo is empty for that bundle Manifest, it indicates the bundle is deleted and should
|
|
// not be added to Manifests list
|
|
if manifest.BundleInfo.Name == "" {
|
|
continue
|
|
}
|
|
manifests = append(manifests, manifest)
|
|
}
|
|
|
|
// Set included manifests which will be used by file/pkg subtraction
|
|
for _, m := range manifests {
|
|
if m.Name == "full" || m.Name == "os-core" {
|
|
continue
|
|
}
|
|
if err = m.ReadIncludesFromBundleInfo(manifests); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return manifests, nil
|
|
}
|
|
|
|
// mcaPkgInfo downloads and queries all packages for each bundle to collect
|
|
// file metadata that will be used to create subtracted package/file lists.
|
|
func (b *Builder) mcaPkgInfo(manifests []*swupd.Manifest, version, downloadRetries int, repoURLOverrides map[string]string) (map[string]*mcaBundlePkgInfo, error) {
|
|
var dnfConf string
|
|
|
|
// map of repositories and their URIs with resolved $releasever
|
|
repoURIs := make(map[string]string)
|
|
|
|
// Download RPMs from correct upstream version
|
|
upstreamVer, err := b.getLocalUpstreamVersion(strconv.Itoa(version))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if repoURLOverrides != nil {
|
|
// Create tmp DNF conf with overridden baseurl values for specified repos
|
|
tmpConf, err := ioutil.TempFile(os.TempDir(), "mixerTmpConf")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
path := tmpConf.Name()
|
|
_ = tmpConf.Close()
|
|
_ = os.Remove(path)
|
|
}()
|
|
|
|
repoURIs, err = b.WriteRepoURLOverrides(tmpConf, repoURLOverrides)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dnfConf = tmpConf.Name()
|
|
} else {
|
|
dnfConf = b.Config.Builder.DNFConf
|
|
for repo, r := range b.repos {
|
|
repoURIs[repo] = r.url
|
|
}
|
|
}
|
|
|
|
for repo, url := range repoURIs {
|
|
repoURIs[repo] = strings.ReplaceAll(url, "$releasever", upstreamVer)
|
|
}
|
|
|
|
packagerCmd := []string{
|
|
"dnf",
|
|
"-y",
|
|
"--config=" + dnfConf,
|
|
"--releasever=" + upstreamVer,
|
|
}
|
|
|
|
set := make(bundleSet)
|
|
for _, m := range manifests {
|
|
// DirectPackages is used to populate AllPackages because AllPackages contains
|
|
// unnecessary packages from included bundles that would be subtracted away.
|
|
set[m.Name] = &bundle{
|
|
Name: m.BundleInfo.Name,
|
|
AllPackages: m.BundleInfo.DirectPackages,
|
|
}
|
|
}
|
|
|
|
// TODO: resolvePackages is a bottleneck that could be removed by updating the bundleinfo files
|
|
// to contain resolved packages and associate them with their repo, version, and arch.
|
|
|
|
// Resolve the package dependencies and collect the repo, version, and arch for each package
|
|
repoPkgs, err := resolvePackagesValidation(b.NumBundleWorkers, set, packagerCmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Obtain necessary package metadata by querying each rpm
|
|
return b.resolveBundlePkgInfos(manifests, repoPkgs, packagerCmd, repoURIs, downloadRetries)
|
|
}
|
|
|
|
func (b *Builder) resolveBundlePkgInfos(manifests []*swupd.Manifest, repoPkgs *sync.Map, packagerCmd []string, repoURIs map[string]string, downloadRetries int) (map[string]*mcaBundlePkgInfo, error) {
|
|
pkgCh := make(chan *pkgInfo, b.NumBundleWorkers)
|
|
errCh := make(chan error, b.NumBundleWorkers)
|
|
defer close(errCh)
|
|
var wg sync.WaitGroup
|
|
var err error
|
|
pInfo := make(map[string]*mcaBundlePkgInfo)
|
|
|
|
// Duplicate packages re-use the same *pkgInfo
|
|
pkgInfoCache := make(map[string]*pkgInfo)
|
|
|
|
pkgWorker := func() {
|
|
wg.Add(1)
|
|
defer wg.Done()
|
|
|
|
for p := range pkgCh {
|
|
p.files, err = b.resolvePkgFiles(p, downloadRetries)
|
|
if err != nil {
|
|
errCh <- err
|
|
}
|
|
}
|
|
}
|
|
for i := 0; i < b.NumBundleWorkers; i++ {
|
|
go pkgWorker()
|
|
}
|
|
|
|
// TODO: file/package subtraction could be integrated here so that they don't need to be
|
|
// subtracted later.
|
|
|
|
// Query file metadata for each package in repoPkgs and update the allPkgs field for each bundle.
|
|
for _, m := range manifests {
|
|
bundlePkgInfo := &mcaBundlePkgInfo{
|
|
name: m.Name,
|
|
allFiles: make(map[string]bool),
|
|
allPkgs: make(map[string]*pkgInfo),
|
|
}
|
|
pInfo[m.Name] = bundlePkgInfo
|
|
|
|
// repoPkgs is a map of bundles -> map of repos -> list of packageMetadata
|
|
repo, ok := repoPkgs.Load(m.Name)
|
|
if !ok {
|
|
return nil, errors.Errorf("Failed to load bundle %s from map", m.Name)
|
|
}
|
|
|
|
for repo, r := range repo.(repoPkgMap) {
|
|
resolveList := []packageMetadata{}
|
|
for _, p := range r {
|
|
pkg, ok := pkgInfoCache[p.name]
|
|
if !ok {
|
|
pkg = &pkgInfo{
|
|
name: p.name,
|
|
}
|
|
pkgInfoCache[p.name] = pkg
|
|
resolveList = append(resolveList, p)
|
|
}
|
|
bundlePkgInfo.allPkgs[p.name] = pkg
|
|
}
|
|
|
|
// TODO: resolveRpmURIs is a bottleneck when all files are local. Running this function with a long
|
|
// list of packages is significantly faster than running many times with small lists.
|
|
|
|
// Resolve the URI of each new rpm
|
|
if err := resolveRpmURIs(resolveList, repo, repoURIs[repo], pkgInfoCache, packagerCmd); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Query file metadata for each new package
|
|
for _, p := range resolveList {
|
|
pkg, ok := pkgInfoCache[p.name]
|
|
if !ok {
|
|
return nil, errors.Errorf("Failed to load pkg %s from map", p.name)
|
|
}
|
|
select {
|
|
case pkgCh <- pkg:
|
|
case err = <-errCh:
|
|
break
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
close(pkgCh)
|
|
wg.Wait()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(errCh) > 0 {
|
|
return nil, <-errCh
|
|
}
|
|
|
|
// Create map of files in each bundle
|
|
for _, bundlePkgInfo := range pInfo {
|
|
for _, p := range bundlePkgInfo.allPkgs {
|
|
for _, f := range p.files {
|
|
bundlePkgInfo.allFiles[f.name] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return pInfo, nil
|
|
}
|
|
|
|
// resolvePkgFiles queries a package for a list of file metadata.
|
|
func (b *Builder) resolvePkgFiles(pkg *pkgInfo, downloadRetries int) ([]*fileInfo, error) {
|
|
var err error
|
|
var out *bytes.Buffer
|
|
|
|
queryCmd := "[%{filenames}\a%{filesizes}\a%{filedigests}\a%{filemodes:perms}\a%{filelinktos}\a%{fileusername}\a%{filegroupname}\n]"
|
|
rpmCmd := []string{"rpm", "-qp", "--qf=" + queryCmd}
|
|
|
|
// Query RPM for file metadata lists
|
|
args := merge(rpmCmd, pkg.uri)
|
|
for attempts := 0; attempts <= downloadRetries; attempts++ {
|
|
out, err = helpers.RunCommandOutputEnv(log.Dnf, args[0], args[1:], []string{"LC_ALL=en_US.UTF-8"})
|
|
if err == nil {
|
|
break
|
|
}
|
|
if attempts == downloadRetries {
|
|
log.Error(log.Dnf, err.Error(), "download retries: %d", downloadRetries)
|
|
log.Debug(log.Dnf, out.String())
|
|
return nil, errors.New("failed to resolve package metadata")
|
|
}
|
|
log.Debug(log.Dnf, err.Error(), "download retries: %d", downloadRetries)
|
|
}
|
|
|
|
pkgFiles := []*fileInfo{}
|
|
|
|
// Each line contains metadata for a single file in the package
|
|
queryLines := strings.Split(out.String(), "\n")
|
|
for i, line := range queryLines {
|
|
// The last line has no entry
|
|
if i == len(queryLines)-1 {
|
|
continue
|
|
}
|
|
|
|
fileMetadata := strings.Split(line, "\a")
|
|
path := fileMetadata[0]
|
|
|
|
// Paths that are banned from manifests are skipped by MCA
|
|
if isBannedPath(path) {
|
|
continue
|
|
}
|
|
|
|
// Files with blacklisted characters are skipped
|
|
if swupd.FilenameBlacklisted(filepath.Base(path)) {
|
|
continue
|
|
}
|
|
|
|
// Directories are omitted from MCA because they may be missed from rpm output.
|
|
mode := fileMetadata[3]
|
|
if mode[:1] == "d" {
|
|
continue
|
|
}
|
|
|
|
// Some Clear Linux packages install files with path components that are
|
|
// symlinks. MCA must resolve file paths to align with the manifests.
|
|
path = resolveFileName(path)
|
|
|
|
pkgFile := &fileInfo{
|
|
name: path,
|
|
size: fileMetadata[1],
|
|
hash: fileMetadata[2],
|
|
modes: fileMetadata[3],
|
|
links: fileMetadata[4],
|
|
user: fileMetadata[5],
|
|
group: fileMetadata[6],
|
|
hashLen: len(fileMetadata[2]),
|
|
pkg: pkg.name,
|
|
}
|
|
pkgFiles = append(pkgFiles, pkgFile)
|
|
}
|
|
return pkgFiles, nil
|
|
}
|
|
|
|
// resolveRpmURIs resolves the rpm URIs for the pkgList and updates the pkgInfoCache
|
|
func resolveRpmURIs(pkgList []packageMetadata, repo, baseURI string, pkgInfoCache map[string]*pkgInfo, packagerCmd []string) error {
|
|
if len(pkgList) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Query the relative location of the rpm within the repo and the package name.
|
|
// The relative rpm location is appended to the baseURI to determine the uri of
|
|
// the rpm and the package name is used as the pkgInfoCache key to update the map.
|
|
queryCmd := "%{location}\a%{name}\n"
|
|
queryStringRpm := merge(
|
|
packagerCmd,
|
|
"repoquery",
|
|
"--quiet",
|
|
"--repo",
|
|
repo,
|
|
"--qf",
|
|
queryCmd,
|
|
)
|
|
for _, p := range pkgList {
|
|
// Use NVRA (name-version-release.arch) to avoid ambiguous results with
|
|
// multiple packages. The p.version field is formatted as version-release.
|
|
rpmName := p.name + "-" + p.version + "." + p.arch
|
|
queryStringRpm = append(queryStringRpm, rpmName)
|
|
}
|
|
|
|
outBuf, err := helpers.RunCommandOutputEnv(log.Dnf, queryStringRpm[0], queryStringRpm[1:], []string{"LC_ALL=en_US.UTF-8"})
|
|
if err != nil {
|
|
log.Error(log.Dnf, err.Error())
|
|
log.Debug(log.Dnf, outBuf.String())
|
|
return errors.New("unable to resolve rpm URI")
|
|
}
|
|
repoURI, err := url.Parse(baseURI)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Each line contains the relative location of the rpm within the repository and
|
|
// a key for the pkgInfoCache. These values are separated by '\a'.
|
|
queryLines := strings.Split(outBuf.String(), "\n")
|
|
for _, line := range queryLines {
|
|
queryResults := strings.Split(line, "\a")
|
|
if len(queryResults) != 2 {
|
|
continue
|
|
}
|
|
|
|
// Append the relative rpm path to repoURI
|
|
u := *repoURI
|
|
u.Path = path.Join(u.Path, queryResults[0])
|
|
|
|
key := queryResults[1]
|
|
|
|
pkg, ok := pkgInfoCache[key]
|
|
if !ok {
|
|
return errors.Errorf("%s not in pkgInfoCache", key)
|
|
}
|
|
pkg.uri = u.String()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getManFiles collects the manifest's file list
|
|
func (info *mcaBundleInfo) getManFiles(manifest *swupd.Manifest) error {
|
|
for _, file := range manifest.Files {
|
|
// Skip directories and deleted/ghosted files
|
|
if file.Type == swupd.TypeDirectory ||
|
|
file.Status == swupd.StatusDeleted ||
|
|
file.Status == swupd.StatusGhosted {
|
|
|
|
continue
|
|
}
|
|
|
|
info.manFiles[file.Name] = file
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getSubPkgs gets the bundle's subtracted packages list
|
|
func (info *mcaBundleInfo) getSubPkgs(manifest *swupd.Manifest, pInfo map[string]*mcaBundlePkgInfo) error {
|
|
includes := manifest.GetRecursiveIncludes()
|
|
|
|
for p := range pInfo[manifest.Name].allPkgs {
|
|
isIncluded := false
|
|
|
|
for _, inc := range includes {
|
|
if pInfo[inc.Name].allPkgs[p] != nil {
|
|
isIncluded = true
|
|
break
|
|
}
|
|
}
|
|
if !isIncluded {
|
|
info.subPkgs[p] = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getSubPkgFiles gets the subtracted files that were resolved from RPMs
|
|
func (info *mcaBundleInfo) getSubPkgFiles(manifest *swupd.Manifest, pInfo map[string]*mcaBundlePkgInfo) error {
|
|
includes := manifest.GetRecursiveIncludes()
|
|
|
|
// Collect all files from bundle's subtracted packages
|
|
for p := range info.subPkgs {
|
|
for _, f := range pInfo[manifest.Name].allPkgs[p].files {
|
|
isIncluded := false
|
|
|
|
for _, inc := range includes {
|
|
if pInfo[inc.Name].allFiles[f.name] {
|
|
isIncluded = true
|
|
break
|
|
}
|
|
}
|
|
if !isIncluded {
|
|
info.subPkgFiles[f.name] = f
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// diffMcaInfo calculates the manifest file, package, and resolved package file
|
|
// differences between two versions and captures metadata required by printMcaResults.
|
|
func diffMcaInfo(fromInfo, toInfo map[string]*mcaBundleInfo) (*mcaDiffResults, error) {
|
|
results := &mcaDiffResults{}
|
|
|
|
// Bundles in toInfo are either added, modified, or unchanged
|
|
for bName := range toInfo {
|
|
bundleDiff := &mcaBundleDiff{
|
|
name: bName,
|
|
pkgFileCounts: make(map[string]*bundlePkgStats),
|
|
}
|
|
|
|
// Initialize packages file counts to 0
|
|
for p := range toInfo[bName].subPkgs {
|
|
bundleDiff.pkgFileCounts[p] = &bundlePkgStats{}
|
|
}
|
|
if fromInfo[bName] != nil {
|
|
for p := range fromInfo[bName].subPkgs {
|
|
if bundleDiff.pkgFileCounts[p] == nil {
|
|
bundleDiff.pkgFileCounts[p] = &bundlePkgStats{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compare bundle in from/to versions
|
|
if fromInfo[bName] != nil {
|
|
// Bundles match
|
|
err := bundleDiff.diffBundles(bName, fromInfo, toInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if bundleDiff.status == modified {
|
|
results.modList = append(results.modList, bName)
|
|
}
|
|
} else {
|
|
// Bundle added
|
|
err := bundleDiff.addBundle(bName, toInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results.addList = append(results.addList, bName)
|
|
}
|
|
|
|
results.bundleDiff = append(results.bundleDiff, bundleDiff)
|
|
}
|
|
|
|
for bName := range fromInfo {
|
|
if toInfo[bName] == nil {
|
|
// Bundle deleted
|
|
bundleDiff := &mcaBundleDiff{
|
|
name: bName,
|
|
status: removed,
|
|
}
|
|
|
|
results.delList = append(results.delList, bName)
|
|
results.bundleDiff = append(results.bundleDiff, bundleDiff)
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// diffBundles calculates resolved package file, package, and manifest file
|
|
// diffs between two versions and updates package file statistics.
|
|
func (bundleDiff *mcaBundleDiff) diffBundles(bundle string, fromInfo, toInfo map[string]*mcaBundleInfo) error {
|
|
// File changes caused by the md5 to sha256 hash calc transition are skipped
|
|
skipMap := getSkippedFiles(fromInfo[bundle].subPkgFiles, toInfo[bundle].subPkgFiles)
|
|
|
|
// Diff package files
|
|
diffList := getFileDiffLists(fromInfo[bundle].subPkgFiles, toInfo[bundle].subPkgFiles, skipMap)
|
|
bundleDiff.pkgFileDiffs = diffList
|
|
|
|
// For every added, modified, or deleted file, update package file statistics
|
|
for _, f := range diffList.addList {
|
|
pkg := toInfo[bundle].subPkgFiles[f].pkg
|
|
bundleDiff.pkgFileCounts[pkg].add++
|
|
}
|
|
for _, f := range diffList.modList {
|
|
pkg := toInfo[bundle].subPkgFiles[f].pkg
|
|
bundleDiff.pkgFileCounts[pkg].mod++
|
|
}
|
|
for _, f := range diffList.delList {
|
|
pkg := fromInfo[bundle].subPkgFiles[f].pkg
|
|
bundleDiff.pkgFileCounts[pkg].del++
|
|
}
|
|
|
|
// diff pkgs
|
|
diffList = getPkgDiffLists(fromInfo[bundle].subPkgs, toInfo[bundle].subPkgs, bundleDiff)
|
|
bundleDiff.pkgDiffs = diffList
|
|
|
|
// diff manifest files
|
|
isMinversion, diffList := getManFileDiffLists(fromInfo[bundle].manFiles, toInfo[bundle].manFiles, skipMap)
|
|
bundleDiff.manFileDiffs = diffList
|
|
|
|
// When there is a file change, the bundle is modified
|
|
bundleDiff.minversion = isMinversion
|
|
|
|
if isBundleMod(diffList) {
|
|
bundleDiff.status = modified
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getSkippedFiles returns a map of files to skip for the MCA check. rpm 4.12 used
|
|
// md5 file hashes by default and rpm 4.14 switched the default to sha256. As a result,
|
|
// in build 31680, rpms started to use sha256 hashes instead of md5. Since MCA assumes
|
|
// that hash changes indicate content changes, rpm comparisons with different hash types
|
|
// are not possible without expanding the scope of MCA to verify content hashes.
|
|
func getSkippedFiles(fromFiles, toFiles map[string]*fileInfo) map[string]bool {
|
|
skipMap := make(map[string]bool)
|
|
for fName, toFile := range toFiles {
|
|
// The os-release file is expected to change every build, so a change caused
|
|
// by a different hashing algorithm will not cause an MCA error.
|
|
if fName == "/usr/lib/os-release" {
|
|
continue
|
|
}
|
|
|
|
// Flag comparisons between md5 hashes (len == 32) and sha256 hashes (len == 64),
|
|
// so they can be skipped later. Symlinks have a hashLen of 0 and are not skipped.
|
|
fromFile := fromFiles[fName]
|
|
if fromFile != nil {
|
|
if (fromFile.hashLen == 32 || fromFile.hashLen == 64) &&
|
|
(toFile.hashLen == 32 || toFile.hashLen == 64) &&
|
|
fromFile.hashLen != toFile.hashLen {
|
|
|
|
skipMap[fName] = true
|
|
}
|
|
}
|
|
}
|
|
return skipMap
|
|
}
|
|
|
|
// isBundleMod returns true when a manifest diffList is changed
|
|
func isBundleMod(lists diffLists) bool {
|
|
return (len(lists.addList) + len(lists.modList) + len(lists.delList)) != 0
|
|
}
|
|
|
|
// addBundle marks a bundle's resolved package files, packages, and manifest
|
|
// files as added.
|
|
func (bundleDiff *mcaBundleDiff) addBundle(bundle string, toInfo map[string]*mcaBundleInfo) error {
|
|
fDiffList := diffLists{}
|
|
pDiffList := diffLists{}
|
|
mDiffList := diffLists{}
|
|
|
|
// Add package files
|
|
for _, file := range toInfo[bundle].subPkgFiles {
|
|
pkg := file.pkg
|
|
bundleDiff.pkgFileCounts[pkg].add++
|
|
|
|
fDiffList.addList = append(fDiffList.addList, file.name)
|
|
}
|
|
bundleDiff.pkgFileDiffs = fDiffList
|
|
|
|
// Add packages
|
|
for p := range toInfo[bundle].subPkgs {
|
|
pDiffList.addList = append(pDiffList.addList, p)
|
|
}
|
|
bundleDiff.pkgDiffs = pDiffList
|
|
|
|
// Add manifest
|
|
for f := range toInfo[bundle].manFiles {
|
|
mDiffList.addList = append(mDiffList.addList, f)
|
|
}
|
|
bundleDiff.manFileDiffs = mDiffList
|
|
|
|
// Mark bundle added
|
|
bundleDiff.status = added
|
|
|
|
return nil
|
|
}
|
|
|
|
func getFileDiffLists(fromFiles, toFiles map[string]*fileInfo, skipMap map[string]bool) diffLists {
|
|
addList := []string{}
|
|
delList := []string{}
|
|
modList := []string{}
|
|
|
|
for f := range toFiles {
|
|
if skipMap[f] {
|
|
continue
|
|
}
|
|
if fromFiles[f] != nil {
|
|
// Match
|
|
if isFileMod(fromFiles[f], toFiles[f]) {
|
|
modList = append(modList, f)
|
|
}
|
|
} else {
|
|
// Added file
|
|
addList = append(addList, f)
|
|
}
|
|
}
|
|
|
|
for f := range fromFiles {
|
|
if toFiles[f] == nil {
|
|
// Deleted file
|
|
delList = append(delList, f)
|
|
}
|
|
}
|
|
return diffLists{modList: modList, addList: addList, delList: delList}
|
|
}
|
|
|
|
func getPkgDiffLists(fromPkgs, toPkgs map[string]bool, bundleDiff *mcaBundleDiff) diffLists {
|
|
addList := []string{}
|
|
delList := []string{}
|
|
modList := []string{}
|
|
|
|
for p := range toPkgs {
|
|
if fromPkgs[p] {
|
|
// Match
|
|
if isPkgMod(p, bundleDiff) {
|
|
modList = append(modList, p)
|
|
}
|
|
} else {
|
|
// Added pkg
|
|
addList = append(addList, p)
|
|
}
|
|
}
|
|
|
|
for p := range fromPkgs {
|
|
if !toPkgs[p] {
|
|
// Deleted pkg
|
|
delList = append(delList, p)
|
|
}
|
|
}
|
|
return diffLists{modList: modList, addList: addList, delList: delList}
|
|
}
|
|
|
|
func getManFileDiffLists(fromFiles, toFiles map[string]*swupd.File, skipMap map[string]bool) (bool, diffLists) {
|
|
addList := []string{}
|
|
delList := []string{}
|
|
modList := []string{}
|
|
minversion := false
|
|
|
|
for f := range toFiles {
|
|
if skipMap[f] {
|
|
continue
|
|
}
|
|
if fromFiles[f] != nil {
|
|
// Match
|
|
if isManFileMod(fromFiles[f], toFiles[f]) {
|
|
modList = append(modList, f)
|
|
} else if !minversion {
|
|
minversion = isMinversion(fromFiles[f], toFiles[f])
|
|
}
|
|
} else {
|
|
// Added file
|
|
addList = append(addList, f)
|
|
}
|
|
}
|
|
|
|
for f := range fromFiles {
|
|
if toFiles[f] == nil {
|
|
// Deleted file
|
|
delList = append(delList, f)
|
|
}
|
|
}
|
|
return minversion, diffLists{modList: modList, addList: addList, delList: delList}
|
|
}
|
|
|
|
func isFileMod(from, to *fileInfo) bool {
|
|
return (from.hash != to.hash) ||
|
|
(from.modes != to.modes) ||
|
|
(from.links != to.links) ||
|
|
(from.user != to.user) ||
|
|
(from.group != to.group)
|
|
}
|
|
|
|
func isPkgMod(key string, bundleDiff *mcaBundleDiff) bool {
|
|
// When file(s) associated with a package are modified, the package is modified
|
|
return (bundleDiff.pkgFileCounts[key].add + bundleDiff.pkgFileCounts[key].mod + bundleDiff.pkgFileCounts[key].del) != 0
|
|
}
|
|
|
|
func isManFileMod(from, to *swupd.File) bool {
|
|
return (from.Hash != to.Hash)
|
|
}
|
|
|
|
func isMinversion(from, to *swupd.File) bool {
|
|
return (from.Name == to.Name) &&
|
|
(from.Hash == to.Hash) &&
|
|
(from.Version != to.Version)
|
|
}
|
|
|
|
// analyzeMcaResults compares manifest file changes against package file changes.
|
|
// When there are inconsistencies between the manifest and package file lists,
|
|
// a slice of error strings is returned.
|
|
func analyzeMcaResults(builder *Builder, results *mcaDiffResults, fromInfo, toInfo map[string]*mcaBundleInfo, fromVer, toVer int) ([]string, []string, error) {
|
|
var errorList = []string{}
|
|
|
|
// Compare manifest files against package files. Inconsistencies are added to the errorList.
|
|
for _, b := range results.bundleDiff {
|
|
// Removed bundles will have no corresponding packages
|
|
if b.status == removed {
|
|
continue
|
|
}
|
|
|
|
// Typically, Mixer updates the /usr/lib/os-release file, so an error is generated
|
|
// when it is unchanged. When comparing the +10 to the +20, this error will be
|
|
// removed by removeMcaErrorExceptions.
|
|
if b.name == "os-core" {
|
|
releaseFileMod := false
|
|
for _, file := range b.manFileDiffs.modList {
|
|
if file == "/usr/lib/os-release" {
|
|
releaseFileMod = true
|
|
break
|
|
}
|
|
}
|
|
if !releaseFileMod {
|
|
errorList = append(errorList, "ERROR: /usr/lib/os-release is not modified in manifest 'os-core'\n")
|
|
}
|
|
}
|
|
|
|
errorList = append(errorList, diffResultLists(b, toInfo[b.name].subPkgFiles, "added")...)
|
|
errorList = append(errorList, diffResultLists(b, toInfo[b.name].subPkgFiles, "modified")...)
|
|
|
|
// Added bundles will not exist in the fromInfo object and added bundles cannot have deleted files.
|
|
if fromInfo[b.name] != nil {
|
|
errorList = append(errorList, diffResultLists(b, fromInfo[b.name].subPkgFiles, "deleted")...)
|
|
}
|
|
}
|
|
|
|
// Remove errors generated by special case manifest files that have no package
|
|
// equivalent. Any missing special case files will generate additional errors.
|
|
return checkMcaFbErrors(builder, errorList, fromVer, toVer)
|
|
}
|
|
|
|
// diffResultLists compares manifest files against package files and generates
|
|
// an error string when they don't match.
|
|
func diffResultLists(bundleDiff *mcaBundleDiff, info map[string]*fileInfo, mode string) []string {
|
|
var pkgFiles []string
|
|
var manFiles []string
|
|
|
|
switch mode {
|
|
case "added":
|
|
pkgFiles = bundleDiff.pkgFileDiffs.addList
|
|
manFiles = bundleDiff.manFileDiffs.addList
|
|
case "modified":
|
|
pkgFiles = bundleDiff.pkgFileDiffs.modList
|
|
manFiles = bundleDiff.manFileDiffs.modList
|
|
case "deleted":
|
|
pkgFiles = bundleDiff.pkgFileDiffs.delList
|
|
manFiles = bundleDiff.manFileDiffs.delList
|
|
}
|
|
|
|
sort.Strings(pkgFiles)
|
|
sort.Strings(manFiles)
|
|
|
|
var errorList = []string{}
|
|
|
|
trackingFileFound := false
|
|
trackingFile := "/usr/share/clear/bundles/" + bundleDiff.name
|
|
|
|
// Compare manifest and package file lists. When they don't match, add an
|
|
// error string to the error slice.
|
|
i := 0
|
|
j := 0
|
|
for i < len(pkgFiles) || j < len(manFiles) {
|
|
var fileComp int
|
|
if i >= len(pkgFiles) {
|
|
fileComp = 1
|
|
} else if j >= len(manFiles) {
|
|
fileComp = -1
|
|
} else {
|
|
fileComp = strings.Compare(pkgFiles[i], manFiles[j])
|
|
}
|
|
|
|
switch fileComp {
|
|
case 1:
|
|
// File in manifest list, but not package
|
|
if manFiles[j] == trackingFile && mode == "added" && bundleDiff.status == added {
|
|
// Each manifest should have a tracking file that will not be
|
|
// included in a package
|
|
trackingFileFound = true
|
|
} else {
|
|
errorMsg := fmt.Sprintf("ERROR: %s is %s in manifest '%s', but not in a package\n", manFiles[j], mode, bundleDiff.name)
|
|
errorList = append(errorList, errorMsg)
|
|
}
|
|
j++
|
|
case -1:
|
|
// File in package list, but not manifest
|
|
errorMsg := fmt.Sprintf("ERROR: %s is %s in package '%s', but not %s in manifest '%s'\n", pkgFiles[i], mode, info[pkgFiles[i]].pkg, mode, bundleDiff.name)
|
|
errorList = append(errorList, errorMsg)
|
|
i++
|
|
case 0:
|
|
i++
|
|
j++
|
|
}
|
|
}
|
|
|
|
// Create error when new bundle doesn't add a bundle tracking file.
|
|
if !trackingFileFound && mode == "added" && bundleDiff.status == added {
|
|
errorMsg := fmt.Sprintf("ERROR: %s is missing from manifest '%s'\n", trackingFile, bundleDiff.name)
|
|
errorList = append(errorList, errorMsg)
|
|
}
|
|
|
|
return errorList
|
|
}
|
|
|
|
// checkMcaFbErrors checks for error messages generated by special case manifest
|
|
// files that are not modified by packages.
|
|
func checkMcaFbErrors(b *Builder, diffErrors []string, fromVer, toVer int) ([]string, []string, error) {
|
|
var errorList = []string{}
|
|
var warningList = []string{}
|
|
|
|
fromPlus10, err := b.isPlus10Version(fromVer)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
toPlus10, err := b.isPlus10Version(toVer)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
formatMatch, err := b.checkFormatsMatch(fromVer, toVer)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// The release file should be modified with the exception of the +10 to +20
|
|
// comparison. This value will be overridden when the file is unchanged.
|
|
releaseFileMod := true
|
|
|
|
// Special case files that can be modified in a manifest, but have no package
|
|
// equivalent will generate false positive errors. Remove the false positive
|
|
// errors and generate error/warning messages when expected special case errors
|
|
// are missing.
|
|
var versionFile, versionstampFile, formatFile bool
|
|
for _, err := range diffErrors {
|
|
switch err {
|
|
// Mixer modifies the version field in the /usr/lib/os-release file, but the
|
|
// file also exists in the filesystem package. When the filesystem package
|
|
// is modified, the false positive error will not be generated. To verify that
|
|
// os-release is modified when not comparing the +10 to the +20, the below error
|
|
// message is tracked in addition to the false positives.
|
|
case "ERROR: /usr/lib/os-release is not modified in manifest 'os-core'\n":
|
|
releaseFileMod = false
|
|
case "ERROR: /usr/lib/os-release is modified in manifest 'os-core', but not in a package\n":
|
|
continue
|
|
case "ERROR: /usr/share/clear/version is modified in manifest 'os-core', but not in a package\n":
|
|
versionFile = true
|
|
case "ERROR: /usr/share/clear/versionstamp is modified in manifest 'os-core', but not in a package\n":
|
|
versionstampFile = true
|
|
case "ERROR: /usr/share/defaults/swupd/format is modified in manifest 'os-core-update', but not in a package\n":
|
|
if toPlus10 || !formatMatch {
|
|
formatFile = true
|
|
}
|
|
default:
|
|
errorList = append(errorList, err)
|
|
}
|
|
}
|
|
if fromPlus10 {
|
|
// A comparison between +10 -> +20 versions is the only valid case when no changes
|
|
// are expected in os-core/os-core-update, but the +20 version cannot be detected since Mixer does
|
|
// not track the first file in a format. As a result, assume this case is a +10 -> +20
|
|
// comparison and print a warning message.
|
|
if !releaseFileMod && !versionFile && !versionstampFile && !formatFile {
|
|
warningList = append(warningList, "WARNING: If this is not a +10 to +20 comparison, expected file changes are missing from os-core/os-core-update\n")
|
|
return errorList, warningList, nil
|
|
}
|
|
warningList = append(warningList, "WARNING: If this is a +10 to +20 comparison, os-core/os-core-update have file exception errors\n")
|
|
}
|
|
|
|
// When the comparison is not between +10 -> +20 versions, re-add an error when /usr/lib/os-release
|
|
// is not modified
|
|
if !releaseFileMod {
|
|
errorList = append(errorList, "ERROR: /usr/lib/os-release is not modified in manifest 'os-core'\n")
|
|
}
|
|
if !versionFile {
|
|
errorList = append(errorList, "ERROR: /usr/share/clear/version is not modified in manifest 'os-core'\n")
|
|
}
|
|
if !versionstampFile {
|
|
errorList = append(errorList, "ERROR: /usr/share/clear/versionstamp is not modified in manifest 'os-core'\n")
|
|
}
|
|
|
|
if (toPlus10 || !formatMatch) && !formatFile {
|
|
if fromPlus10 {
|
|
warningList = append(warningList, "WARNING: If comparing +10 to a version across multiple format boundaries, the format file in 'os-core-update' must be modified\n")
|
|
} else {
|
|
errorList = append(errorList, "ERROR: /usr/share/defaults/swupd/format is not modified in manifest 'os-core-update'\n")
|
|
}
|
|
}
|
|
|
|
return errorList, warningList, nil
|
|
}
|
|
|
|
// isPlus10Version determines whether the version is the last version in a format.
|
|
func (b *Builder) isPlus10Version(ver int) (bool, error) {
|
|
verStr := strconv.Itoa(ver)
|
|
format, err := b.getFormatForVersion(verStr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
latest, err := b.getLatestForFormat(format)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// +10 is last version in a completed format
|
|
if format < b.State.Mix.Format && verStr == latest {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// checkFormatsMatch determines whether two versions are in the same format.
|
|
func (b *Builder) checkFormatsMatch(fromVer, toVer int) (bool, error) {
|
|
fromFormat, err := b.getFormatForVersion(strconv.Itoa(fromVer))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
toFormat, err := b.getFormatForVersion(strconv.Itoa(toVer))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if fromFormat == toFormat {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// printMcaResults displays any MCA errors and prints bundle diff statistics when there are no errors.
|
|
func printMcaResults(results *mcaDiffResults, fromInfo, toInfo map[string]*mcaBundleInfo, fromVer, toVer, tableWidth int, errorList, warningList []string) error {
|
|
// Print any warnings
|
|
for _, msg := range warningList {
|
|
log.Warning(log.Mca, "%s", msg)
|
|
}
|
|
// An overwhelming number of errors can be generated when this test
|
|
// identifies a manifest bug, so limit the error output to 50.
|
|
if len(errorList) > 0 {
|
|
for i, err := range errorList {
|
|
if i == 50 {
|
|
log.Warning(log.Mca, " Error reporting is limited to 50, so additional errors were skipped.")
|
|
break
|
|
}
|
|
log.Error(log.Mca, err)
|
|
}
|
|
return errors.New("manifest errors were identified")
|
|
}
|
|
|
|
log.Info(log.Mca, "** Summary: No errors detected in manifests\n")
|
|
log.Info(log.Mca, "Stats for manifests, from build %d to %d\n", fromVer, toVer)
|
|
|
|
// Print bundle counts and lists of added/deleted bundles.
|
|
log.Info(log.Mca, "Added bundles: %d", len(results.addList))
|
|
sort.Strings(results.addList)
|
|
for _, b := range results.addList {
|
|
log.Info(log.Mca, " - %s", b)
|
|
}
|
|
|
|
log.Info(log.Mca, "Changed bundles: %d", len(results.modList))
|
|
|
|
log.Info(log.Mca, "Deleted bundles: %d", len(results.delList))
|
|
sort.Strings(results.delList)
|
|
for _, b := range results.delList {
|
|
log.Info(log.Mca, " - %s", b)
|
|
}
|
|
|
|
// No bundle information to print
|
|
if len(results.bundleDiff) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Skip statistics table when tableWidth too small
|
|
if tableWidth < MinMcaTableWidth {
|
|
return nil
|
|
}
|
|
|
|
// The results table requires 7 characters to build 2 column table. The bundle
|
|
// column character limit is set to 25% of the available characters and the changes
|
|
// column is set to the remaining 75% of available characters.
|
|
bundleWidth := (tableWidth - 7) / 4
|
|
changesWidth := tableWidth - 7 - bundleWidth
|
|
|
|
// Create table with separator rows after each bundle entry. The table's text wrapping
|
|
// does not work well, so text wrapping is handled by the appendMcaTableEntry function.
|
|
var buf bytes.Buffer
|
|
table := tablewriter.NewWriter(&buf)
|
|
table.SetRowLine(true)
|
|
table.SetAutoWrapText(false)
|
|
table.SetHeader([]string{"BUNDLE", "CHANGES"})
|
|
|
|
sort.Slice(results.bundleDiff, func(i, j int) bool {
|
|
return results.bundleDiff[i].name < results.bundleDiff[j].name
|
|
})
|
|
|
|
// Print statistics for each bundle
|
|
for _, b := range results.bundleDiff {
|
|
var entryLine string
|
|
|
|
// Skip unchanged and deleted bundles
|
|
if (b.status == unchanged || b.status == removed) && !b.minversion {
|
|
continue
|
|
}
|
|
changeStr := appendMcaTableEntry("", "Summary:", changesWidth)
|
|
|
|
if b.minversion {
|
|
changeStr = appendMcaTableEntry(changeStr, "** Minversion bump detected", changesWidth)
|
|
}
|
|
|
|
// Print bundle sizes in MB and calculate bundle content size
|
|
// change as percentage.
|
|
toSize := float64(toInfo[b.name].size) / 1048576
|
|
|
|
if fromInfo[b.name] == nil || fromInfo[b.name].size == 0 {
|
|
// Print added bundle size
|
|
entryLine = fmt.Sprintf("Size: %.1fMB", toSize)
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
} else {
|
|
fromSize := float64(fromInfo[b.name].size) / 1048576
|
|
sizeChange := ((toSize / fromSize) - 1) * 100
|
|
|
|
if sizeChange <= float64(-0.01) {
|
|
entryLine = fmt.Sprintf("Size change: %.1fMB -> %.1fMB (%.2f%%)", fromSize, toSize, sizeChange)
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
} else if sizeChange >= float64(0.01) {
|
|
entryLine = fmt.Sprintf("Size change: %.1fMB -> %.1fMB (+%.2f%%)", fromSize, toSize, sizeChange)
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
} else {
|
|
changeStr = appendMcaTableEntry(changeStr, "Size change: (none)", changesWidth)
|
|
}
|
|
}
|
|
|
|
// Print bundle file statistics
|
|
entryLine = fmt.Sprintf("Files added: %d", len(b.manFileDiffs.addList))
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
|
|
entryLine = fmt.Sprintf("Files changed: %d", len(b.manFileDiffs.modList))
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
|
|
entryLine = fmt.Sprintf("Files deleted: %d", len(b.manFileDiffs.delList))
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
|
|
// Print added and deleted packages for bundle
|
|
if len(b.pkgDiffs.addList) > 0 {
|
|
changeStr = appendMcaTableEntry(changeStr, "Packages added:", changesWidth)
|
|
sort.Strings(b.pkgDiffs.addList)
|
|
}
|
|
|
|
for _, p := range b.pkgDiffs.addList {
|
|
entryLine := fmt.Sprintf(" %s", p)
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
}
|
|
if len(b.pkgDiffs.delList) > 0 {
|
|
changeStr = appendMcaTableEntry(changeStr, "Packages deleted:", changesWidth)
|
|
sort.Strings(b.pkgDiffs.delList)
|
|
}
|
|
|
|
for _, p := range b.pkgDiffs.delList {
|
|
entryLine := fmt.Sprintf(" %s", p)
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
}
|
|
|
|
// Print file changes for each package in bundle
|
|
changeStr = appendMcaTableEntry(changeStr, " Changes per package:", changesWidth)
|
|
|
|
pkgList := append(b.pkgDiffs.addList, b.pkgDiffs.delList...)
|
|
pkgList = append(pkgList, b.pkgDiffs.modList...)
|
|
sort.Strings(pkgList)
|
|
|
|
if len(pkgList) == 0 {
|
|
changeStr = appendMcaTableEntry(changeStr, " (none)", changesWidth)
|
|
}
|
|
|
|
for _, p := range pkgList {
|
|
if b.pkgFileCounts[p] == nil {
|
|
continue
|
|
}
|
|
if (b.pkgFileCounts[p].add + b.pkgFileCounts[p].mod + b.pkgFileCounts[p].del) == 0 {
|
|
continue
|
|
}
|
|
entryLine = fmt.Sprintf(" %s (%d added, %d changed, %d deleted)", p, b.pkgFileCounts[p].add, b.pkgFileCounts[p].mod, b.pkgFileCounts[p].del)
|
|
changeStr = appendMcaTableEntry(changeStr, entryLine, changesWidth)
|
|
}
|
|
bundleStr := appendMcaTableEntry("", b.name, bundleWidth)
|
|
table.Append([]string{bundleStr, changeStr})
|
|
}
|
|
|
|
log.Info(log.Mca, "Files changed per manifest:\n")
|
|
|
|
table.Render()
|
|
log.Info(log.Mca, buf.String())
|
|
return nil
|
|
}
|
|
|
|
// appendMcaTableEntry adds a new row to an MCA results table entry. Rows longer
|
|
// than the maxLen will be wrapped to the next line.
|
|
func appendMcaTableEntry(baseStr, newStr string, maxLen int) string {
|
|
if len(baseStr) > 0 {
|
|
baseStr += "\n"
|
|
}
|
|
|
|
for len(newStr) > maxLen {
|
|
baseStr += newStr[0:maxLen] + "\n"
|
|
newStr = newStr[maxLen:]
|
|
}
|
|
if len(newStr) > 0 {
|
|
baseStr += newStr
|
|
}
|
|
return baseStr
|
|
}
|