Compare commits

...

31 Commits

Author SHA1 Message Date
Ettore Di Giacinto
d472dee19b Tag 0.9.14 2020-12-09 19:33:11 +01:00
Ettore Di Giacinto
b5990b5333 Generate changes from CompilerBackendOptions and pass by image name so img can unpack images 2020-12-09 00:27:37 +01:00
Ettore Di Giacinto
767488327b Update README 2020-12-08 17:48:21 +01:00
Ettore Di Giacinto
be3933998b Update links in README 2020-12-08 17:47:33 +01:00
Ettore Di Giacinto
fa9dc9da53 Tag 0.9.13 2020-12-08 15:49:40 +01:00
Ettore Di Giacinto
9911888d18 Stabilize test 2020-12-08 14:56:51 +01:00
Ettore Di Giacinto
2906180c43 Upgrade universe test expectations are changed 2020-12-08 14:15:52 +01:00
Ettore Di Giacinto
cf5e4e1305 Detect removed also when availables aren't found 2020-12-08 12:28:20 +01:00
Ettore Di Giacinto
519586f6bc Search for removed in Def DB 2020-12-08 12:07:28 +01:00
Ettore Di Giacinto
6dbc422b8f Apply solver change to UpgradeUniverse also to the parallel variant and adapt tests
Similarly, we want just to consider what is being uninstalled and the
new rules of the package that is going to be upgraded
2020-12-08 11:43:38 +01:00
Ettore Di Giacinto
a3cfebf438 Create BuildFormula from installed with InstallDatabase
Instead of using the DefinitionDB which supposedly contains only the
relations present in the online repositories. In this way the solver its
more consistent and tries to solve with only the internal definitions.

This also fixes quirks with luet upgrade --universe
2020-12-08 10:58:08 +01:00
Ettore Di Giacinto
24201b25ef Apply solver change also to the parallel variant 2020-12-08 10:41:03 +01:00
Ettore Di Giacinto
7c53296530 Adapt tests 2020-12-08 10:39:15 +01:00
Ettore Di Giacinto
a3cb0ed17f When attempting to uninstall, do it from the internal db so it can resolve the current versions 2020-12-08 02:04:54 +01:00
Ettore Di Giacinto
9ca5d24856 Tag 0.9.12 2020-12-07 20:18:49 +01:00
Ettore Di Giacinto
9a34296be0 Build step is always required for tagging images 2020-12-07 19:39:56 +01:00
Ettore Di Giacinto
ebd18ae22c Set builderTagged image afterwards 2020-12-07 18:58:14 +01:00
Ettore Di Giacinto
7f10a19be5 Don't hide build output 2020-12-07 18:56:39 +01:00
Ettore Di Giacinto
6bf7368993 Don't replace buildertaggedImage if there aren't build steps 2020-12-07 18:39:15 +01:00
Ettore Di Giacinto
338f310d67 Tag and push an image when virtual is supplied, to have a track of it in the image graph tree 2020-12-07 17:59:30 +01:00
Ettore Di Giacinto
3fd1bdbfc8 ADD automatically extracts as well 2020-12-07 17:21:06 +01:00
Ettore Di Giacinto
59d78c3f5c While upgrading always use nodeps while computing uninstall 2020-12-07 17:20:55 +01:00
Ettore Di Giacinto
86c256a062 Generate empty tar 2020-12-07 17:20:32 +01:00
Ettore Di Giacinto
876e3659fb Turn full off by default on upgrade 2020-12-07 00:48:28 +01:00
Ettore Di Giacinto
3c0dd2b71d Adapt test 2020-12-07 00:07:57 +01:00
Ettore Di Giacinto
e9b4d66a3e Retrieve should be rendered also for step images 2020-12-07 00:00:32 +01:00
Ettore Di Giacinto
5047316b70 Try to build only when strictly necessary 2020-12-06 23:50:51 +01:00
Ettore Di Giacinto
02edc10c58 Tag 0.9.11 2020-12-06 22:52:15 +01:00
Ettore Di Giacinto
d479ada402 Don't consider deps while uninstalling during package Swap
Beside being forced, it also doesn't need to look deep into the deps, as
we already have precalculated those
2020-12-06 22:48:48 +01:00
Ettore Di Giacinto
7b800c9a20 Pre-compute swap step
Otherwise, while upgrading, it could happen that package dependencies
aren't downloaded before, and they would just be installed in the middle
of installation, after removal already happened.
2020-12-06 22:11:17 +01:00
Ettore Di Giacinto
18e6e085d5 Sort correctly also subfolders 2020-12-05 23:17:05 +01:00
23 changed files with 407 additions and 133 deletions

View File

@@ -8,7 +8,7 @@
Luet is a multi-platform Package Manager based off from containers - it uses Docker (and others) to build packages. It has zero dependencies and it is well suitable for "from scratch" environments. It can also version entire rootfs and enables delivery of OTA-alike updates, making it a perfect fit for the Edge computing era and IoT embedded devices.
It offers a simple [specfile format](https://luet-lab.github.io/docs/docs/concepts/specfile/) in YAML notation to define both packages and rootfs. As it is based on containers, it can be also used to build stages for Linux From Scratch installations and it can build and track updates for those systems.
It offers a simple [specfile format](https://luet-lab.github.io/docs/docs/concepts/packages/specfile/) in YAML notation to define both [packages](https://luet-lab.github.io/docs/docs/concepts/packages/) and [rootfs](https://luet-lab.github.io/docs/docs/concepts/packages/#package-layers). As it is based on containers, it can be also used to build stages for Linux From Scratch installations and it can build and track updates for those systems.
It is written entirely in Golang and where used as package manager, it can run in from scratch environment, with zero dependencies.
@@ -20,8 +20,8 @@ It is written entirely in Golang and where used as package manager, it can run i
- You can install it aside also with your current distro package manager, and start building and distributing your packages
- Support for packages as "layers"
- [It uses SAT solving techniques to solve the deptree](https://luet-lab.github.io/docs/docs/concepts/constraints/) ( Inspired by [OPIUM](https://ranjitjhala.github.io/static/opium.pdf) )
- Support for collections and templated package definitions
- [Can be extended with Plugins and Extensions](https://luet-lab.github.io/docs/docs/plugins-and-extensions/)
- Support for [collections](https://luet-lab.github.io/docs/docs/concepts/packages/collections/) and [templated package definitions](https://luet-lab.github.io/docs/docs/concepts/packages/templates/)
- [Can be extended with Plugins and Extensions](https://luet-lab.github.io/docs/docs/concepts/plugins-and-extensions/)
## Install

View File

@@ -40,7 +40,7 @@ var Verbose bool
var LockedCommands = []string{"install", "uninstall", "upgrade"}
const (
LuetCLIVersion = "0.9.10"
LuetCLIVersion = "0.9.14"
LuetEnvPrefix = "LUET"
)

View File

@@ -123,7 +123,7 @@ func init() {
upgradeCmd.Flags().Int("solver-attempts", 9000, "Solver maximum attempts")
upgradeCmd.Flags().Bool("force", false, "Force upgrade by ignoring errors")
upgradeCmd.Flags().Bool("nodeps", false, "Don't consider package dependencies (harmful! overrides checkconflicts and full!)")
upgradeCmd.Flags().Bool("full", true, "Attempts to remove as much packages as possible which aren't required (slow)")
upgradeCmd.Flags().Bool("full", false, "Attempts to remove as much packages as possible which aren't required (slow)")
upgradeCmd.Flags().Bool("universe", false, "Use ONLY the SAT solver to compute upgrades (experimental)")
upgradeCmd.Flags().Bool("clean", false, "Try to drop removed packages (experimental, only when --universe is enabled)")
upgradeCmd.Flags().Bool("sync", false, "Upgrade packages with new revisions (experimental)")

View File

@@ -102,15 +102,15 @@ ENV PACKAGE_VERSION=1.4.0
ENV PACKAGE_CATEGORY=app-admin
RUN echo foo > /test
RUN echo bar > /test2`))
opts = CompilerBackendOptions{
opts2 := CompilerBackendOptions{
ImageName: "test",
SourcePath: tmpdir,
DockerFileName: "LuetDockerfile",
Destination: filepath.Join(tmpdir, "output2.tar"),
}
Expect(b.ImageDefinitionToTar(opts)).ToNot(HaveOccurred())
Expect(b.ImageDefinitionToTar(opts2)).ToNot(HaveOccurred())
Expect(helpers.Exists(filepath.Join(tmpdir, "output2.tar"))).To(BeTrue())
diffs, err := b.Changes(filepath.Join(tmpdir2, "output1.tar"), filepath.Join(tmpdir, "output2.tar"))
diffs, err := b.Changes(opts, opts2)
Expect(err).ToNot(HaveOccurred())
artifacts := []ArtifactNode{}

View File

@@ -52,7 +52,10 @@ import (
// }
// }
// ]
func GenerateChanges(b compiler.CompilerBackend, srcImage, dstImage string) ([]compiler.ArtifactLayer, error) {
func GenerateChanges(b compiler.CompilerBackend, fromImage, toImage compiler.CompilerBackendOptions) ([]compiler.ArtifactLayer, error) {
srcImage := fromImage.Destination
dstImage := toImage.Destination
res := compiler.ArtifactLayer{FromImage: srcImage, ToImage: dstImage}
@@ -95,6 +98,7 @@ func GenerateChanges(b compiler.CompilerBackend, srcImage, dstImage string) ([]c
srcImageExtract := compiler.CompilerBackendOptions{
SourcePath: srcImage,
ImageName: fromImage.ImageName,
Destination: srcRootFS,
}
err = b.ExtractRootfs(srcImageExtract, false) // No need to keep permissions as we just collect file diffs
@@ -123,6 +127,7 @@ func GenerateChanges(b compiler.CompilerBackend, srcImage, dstImage string) ([]c
dstImageExtract := compiler.CompilerBackendOptions{
SourcePath: dstImage,
ImageName: toImage.ImageName,
Destination: dstRootFS,
}
err = b.ExtractRootfs(dstImageExtract, false)

View File

@@ -32,12 +32,14 @@ var _ = Describe("Docker image diffs", func() {
Context("Generate diffs from docker images", func() {
It("Detect no changes", func() {
err := b.DownloadImage(compiler.CompilerBackendOptions{
ImageName: "alpine:latest",
})
opts := compiler.CompilerBackendOptions{
ImageName: "alpine:latest",
Destination: "alpine:latest",
}
err := b.DownloadImage(opts)
Expect(err).ToNot(HaveOccurred())
layers, err := GenerateChanges(b, "alpine:latest", "alpine:latest")
layers, err := GenerateChanges(b, opts, opts)
Expect(err).ToNot(HaveOccurred())
Expect(len(layers)).To(Equal(1))
Expect(len(layers[0].Diffs.Additions)).To(Equal(0))
@@ -55,7 +57,13 @@ var _ = Describe("Docker image diffs", func() {
})
Expect(err).ToNot(HaveOccurred())
layers, err := GenerateChanges(b, "quay.io/mocaccino/micro", "quay.io/mocaccino/extra")
layers, err := GenerateChanges(b, compiler.CompilerBackendOptions{
ImageName: "quay.io/mocaccino/micro",
Destination: "quay.io/mocaccino/micro",
}, compiler.CompilerBackendOptions{
ImageName: "quay.io/mocaccino/extra",
Destination: "quay.io/mocaccino/extra",
})
Expect(err).ToNot(HaveOccurred())
Expect(len(layers)).To(Equal(1))

View File

@@ -239,7 +239,7 @@ func (*SimpleDocker) ExtractRootfs(opts compiler.CompilerBackendOptions, keepPer
}
// Changes retrieves changes between image layers
func (d *SimpleDocker) Changes(fromImage, toImage string) ([]compiler.ArtifactLayer, error) {
func (d *SimpleDocker) Changes(fromImage, toImage compiler.CompilerBackendOptions) ([]compiler.ArtifactLayer, error) {
diffs, err := GenerateChanges(d, fromImage, toImage)
if config.LuetCfg.GetGeneral().Debug {

View File

@@ -93,13 +93,13 @@ ENV PACKAGE_VERSION=1.4.0
ENV PACKAGE_CATEGORY=app-admin
RUN echo foo > /test
RUN echo bar > /test2`))
opts = CompilerBackendOptions{
opts2 := CompilerBackendOptions{
ImageName: "test",
SourcePath: tmpdir,
DockerFileName: "LuetDockerfile",
Destination: filepath.Join(tmpdir, "output2.tar"),
}
Expect(b.ImageDefinitionToTar(opts)).ToNot(HaveOccurred())
Expect(b.ImageDefinitionToTar(opts2)).ToNot(HaveOccurred())
Expect(helpers.Exists(filepath.Join(tmpdir, "output2.tar"))).To(BeTrue())
artifacts := []ArtifactNode{}
@@ -109,7 +109,7 @@ RUN echo bar > /test2`))
artifacts = append(artifacts, ArtifactNode{Name: "/test", Size: 4})
artifacts = append(artifacts, ArtifactNode{Name: "/test2", Size: 4})
Expect(b.Changes(filepath.Join(tmpdir2, "output1.tar"), filepath.Join(tmpdir, "output2.tar"))).To(Equal(
Expect(b.Changes(opts, opts2)).To(Equal(
[]ArtifactLayer{{
FromImage: filepath.Join(tmpdir2, "output1.tar"),
ToImage: filepath.Join(tmpdir, "output2.tar"),

View File

@@ -149,7 +149,7 @@ func (*SimpleImg) ExtractRootfs(opts compiler.CompilerBackendOptions, keepPerms
// TODO: Use container-diff (https://github.com/GoogleContainerTools/container-diff) for checking out layer diffs
// Changes uses container-diff (https://github.com/GoogleContainerTools/container-diff) for retrieving out layer diffs
func (i *SimpleImg) Changes(fromImage, toImage string) ([]compiler.ArtifactLayer, error) {
func (i *SimpleImg) Changes(fromImage, toImage compiler.CompilerBackendOptions) ([]compiler.ArtifactLayer, error) {
return GenerateChanges(i, fromImage, toImage)
}

View File

@@ -16,6 +16,7 @@
package compiler
import (
"archive/tar"
"fmt"
"io/ioutil"
"os"
@@ -271,7 +272,7 @@ func (cs *LuetCompiler) unpackDelta(rootfs string, concurrency int, keepPermissi
defer os.Remove(builderOpts.Destination)
}
Info(pkgTag, ":hammer: Generating delta")
diffs, err := cs.Backend.Changes(builderOpts.Destination, runnerOpts.Destination)
diffs, err := cs.Backend.Changes(builderOpts, runnerOpts)
if err != nil {
return nil, errors.Wrap(err, "Could not generate changes from layers")
}
@@ -345,6 +346,10 @@ func (cs *LuetCompiler) buildPackageImage(image, buildertaggedImage, packageImag
return builderOpts, runnerOpts, errors.Wrap(err, "Could not generate image definition")
}
if len(p.GetPreBuildSteps()) == 0 {
buildertaggedImage = image
}
// Then we write the step image, which uses the builder one
if err := p.WriteStepImageDefinition(buildertaggedImage, filepath.Join(buildDir, p.GetPackage().GetFingerPrint()+".dockerfile")); err != nil {
return builderOpts, runnerOpts, errors.Wrap(err, "Could not generate image definition")
@@ -392,11 +397,15 @@ func (cs *LuetCompiler) buildPackageImage(image, buildertaggedImage, packageImag
}
return nil
}
Info(pkgTag, ":whale: Generating 'builder' image from", image, "as", buildertaggedImage, "with prelude steps")
if err := buildAndPush(builderOpts); err != nil {
return builderOpts, runnerOpts, errors.Wrap(err, "Could not push image: "+image+" "+builderOpts.DockerFileName)
if len(p.GetPreBuildSteps()) != 0 {
Info(pkgTag, ":whale: Generating 'builder' image from", image, "as", buildertaggedImage, "with prelude steps")
if err := buildAndPush(builderOpts); err != nil {
return builderOpts, runnerOpts, errors.Wrap(err, "Could not push image: "+image+" "+builderOpts.DockerFileName)
}
}
// Even if we might not have any steps to build, we do that so we can tag the image used in this moment and use that to cache it in a registry, or in the system.
// acting as a docker tag.
Info(pkgTag, ":whale: Generating 'package' image from", buildertaggedImage, "as", packageImage, "with build steps")
if err := buildAndPush(runnerOpts); err != nil {
return builderOpts, runnerOpts, errors.Wrap(err, "Could not push image: "+image+" "+builderOpts.DockerFileName)
@@ -420,6 +429,23 @@ func (cs *LuetCompiler) genArtifact(p CompilationSpec, builderOpts, runnerOpts C
unpack = true
}
if len(p.BuildSteps()) == 0 && len(p.GetPreBuildSteps()) == 0 && !unpack {
fakePackage := p.Rel(p.GetPackage().GetFingerPrint() + ".package.tar")
// We can't generate delta in this case. It implies the package is a virtual, and nothing has to be done really
file, err := os.Create(fakePackage)
if err != nil {
return nil, errors.Wrap(err, "Failed creating virtual package")
}
defer file.Close()
tw := tar.NewWriter(file)
defer tw.Close()
artifact := NewPackageArtifact(fakePackage)
artifact.SetCompressionType(cs.CompressionType)
return artifact, nil
}
// prepare folder content of the image with the package compiled inside
if err := cs.Backend.ExportImage(runnerOpts); err != nil {
return nil, errors.Wrap(err, "Failed exporting image")

View File

@@ -79,7 +79,7 @@ type CompilerBackend interface {
BuildImage(CompilerBackendOptions) error
ExportImage(CompilerBackendOptions) error
RemoveImage(CompilerBackendOptions) error
Changes(fromImage, toImage string) ([]ArtifactLayer, error)
Changes(fromImage, toImage CompilerBackendOptions) ([]ArtifactLayer, error)
ImageDefinitionToTar(CompilerBackendOptions) error
ExtractRootfs(opts CompilerBackendOptions, keepPerms bool) error

View File

@@ -247,6 +247,19 @@ ENV PACKAGE_NAME=` + cs.Package.GetName() + `
ENV PACKAGE_VERSION=` + cs.Package.GetVersion() + `
ENV PACKAGE_CATEGORY=` + cs.Package.GetCategory()
if len(cs.Retrieve) > 0 {
for _, s := range cs.Retrieve {
//var file string
// if helpers.IsValidUrl(s) {
// file = s
// } else {
// file = cs.Rel(s)
// }
spec = spec + `
ADD ` + s + ` /luetbuild/`
}
}
for _, s := range cs.Env {
spec = spec + `
ENV ` + s

View File

@@ -173,6 +173,8 @@ WORKDIR /luetbuild
ENV PACKAGE_NAME=a
ENV PACKAGE_VERSION=1.0
ENV PACKAGE_CATEGORY=test
ADD test /luetbuild/
ADD http://www.google.com /luetbuild/
ENV test=1
RUN echo foo > /test
RUN echo bar > /test2`))

View File

@@ -19,6 +19,8 @@ import (
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"
copy "github.com/otiai10/copy"
@@ -41,6 +43,8 @@ func OrderFiles(target string, files []string) ([]string, []string) {
}
}
dirs := []string{}
for _, f := range files {
target := filepath.Join(target, f)
fi, err := os.Lstat(target)
@@ -48,11 +52,16 @@ func OrderFiles(target string, files []string) ([]string, []string) {
continue
}
if m := fi.Mode(); m.IsDir() {
newFiles = append(newFiles, f)
dirs = append(dirs, f)
}
}
return newFiles, notPresent
// Compare how many sub paths there are, and push at the end the ones that have less subpaths
sort.Slice(dirs, func(i, j int) bool {
return len(strings.Split(dirs[i], string(os.PathSeparator))) > len(strings.Split(dirs[j], string(os.PathSeparator)))
})
return append(newFiles, dirs...), notPresent
}
func ListDir(dir string) ([]string, error) {

View File

@@ -60,5 +60,27 @@ var _ = Describe("Helpers", func() {
Expect(ordered).To(Equal([]string{"baz", "bar/foo", "foo", "baz2/foo", "bar", "baz2"}))
Expect(notExisting).To(Equal([]string{"notexisting"}))
})
It("orders correctly when there are folders with folders", func() {
testDir, err := ioutil.TempDir(os.TempDir(), "test")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(testDir)
err = os.MkdirAll(filepath.Join(testDir, "bar"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.MkdirAll(filepath.Join(testDir, "foo"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.MkdirAll(filepath.Join(testDir, "foo", "bar"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.MkdirAll(filepath.Join(testDir, "foo", "baz"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = os.MkdirAll(filepath.Join(testDir, "foo", "baz", "fa"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
ordered, _ := OrderFiles(testDir, []string{"foo", "foo/bar", "bar", "foo/baz/fa", "foo/baz"})
Expect(ordered).To(Equal([]string{"foo/baz/fa", "foo/bar", "foo/baz", "foo", "bar"}))
})
})
})

View File

@@ -84,7 +84,7 @@ func (l *LuetInstaller) computeUpgrade(syncedRepos Repositories, s *System) (pkg
return uninstall, toInstall, errors.Wrap(err, "Failed solving solution for upgrade")
}
} else {
uninstall, solution, err = solv.Upgrade(!l.Options.FullUninstall, l.Options.NoDeps)
uninstall, solution, err = solv.Upgrade(l.Options.FullUninstall, true)
if err != nil {
return uninstall, toInstall, errors.Wrap(err, "Failed solving solution for upgrade")
}
@@ -241,10 +241,6 @@ func (l *LuetInstaller) swap(syncedRepos Repositories, toRemove pkg.Packages, to
syncedRepos.SyncDatabase(allRepos)
toInstall = syncedRepos.ResolveSelectors(toInstall)
if err := l.download(syncedRepos, toInstall); err != nil {
return errors.Wrap(err, "Pre-downloading packages")
}
// We don't want any conflict with the installed to raise during the upgrade.
// In this way we both force uninstalls and we avoid to check with conflicts
// against the current system state which is pending to deletion
@@ -253,8 +249,43 @@ func (l *LuetInstaller) swap(syncedRepos Repositories, toRemove pkg.Packages, to
// now the solver enforces the constraints and explictly denies two packages
// of the same version installed.
forced := l.Options.Force
nodeps := l.Options.NoDeps
l.Options.Force = true
l.Options.NoDeps = true
// First check what would have been done
installedtmp := pkg.NewInMemoryDatabase(false)
for _, i := range s.Database.World() {
_, err := installedtmp.CreatePackage(i)
if err != nil {
return errors.Wrap(err, "Failed create temporary in-memory db")
}
}
systemAfterChanges := &System{Database: installedtmp}
for _, u := range toRemove {
packs, err := l.computeUninstall(u, systemAfterChanges)
if err != nil && !l.Options.Force {
Error("Failed computing uninstall for ", u.HumanReadableString())
return errors.Wrap(err, "computing uninstall "+u.HumanReadableString())
}
for _, p := range packs {
err = systemAfterChanges.Database.RemovePackage(p)
if err != nil {
return errors.Wrap(err, "Failed removing package from database")
}
}
}
match, packages, assertions, allRepos, err := l.computeInstall(syncedRepos, toInstall, systemAfterChanges)
if err != nil {
return errors.Wrap(err, "computing installation")
}
if err := l.download(syncedRepos, match); err != nil {
return errors.Wrap(err, "Pre-downloading packages")
}
for _, u := range toRemove {
err := l.Uninstall(u, s)
@@ -263,13 +294,9 @@ func (l *LuetInstaller) swap(syncedRepos Repositories, toRemove pkg.Packages, to
return errors.Wrap(err, "uninstalling "+u.HumanReadableString())
}
}
l.Options.Force = forced
match, packages, assertions, allRepos, err := l.computeInstall(syncedRepos, toInstall, s)
if err != nil {
return errors.Wrap(err, "computing installation")
}
l.Options.NoDeps = nodeps
return l.install(syncedRepos, match, packages, assertions, allRepos, s)
}
@@ -325,32 +352,8 @@ func (l *LuetInstaller) Install(cp pkg.Packages, s *System) error {
return l.install(syncedRepos, match, packages, assertions, allRepos, s)
}
func (l *LuetInstaller) download(syncedRepos Repositories, cp pkg.Packages) error {
toDownload := map[string]ArtifactMatch{}
func (l *LuetInstaller) download(syncedRepos Repositories, toDownload map[string]ArtifactMatch) error {
// FIXME: This can be optimized. We don't need to re-match this to the repository
// But we could just do it once
// Gathers things to download
for _, currentPack := range cp {
matches := syncedRepos.PackageMatches(pkg.Packages{currentPack})
if len(matches) == 0 {
return errors.New("Failed matching solutions against repository for " + currentPack.HumanReadableString() + " where are definitions coming from?!")
}
A:
for _, artefact := range matches[0].Repo.GetIndex() {
if artefact.GetCompileSpec().GetPackage() == nil {
return errors.New("Package in compilespec empty")
}
if matches[0].Package.Matches(artefact.GetCompileSpec().GetPackage()) {
toDownload[currentPack.GetFingerPrint()] = ArtifactMatch{Package: currentPack, Artifact: artefact, Repository: matches[0].Repo}
break A
}
}
}
// Download packages into cache in parallel.
all := make(chan ArtifactMatch)
@@ -508,25 +511,13 @@ func (l *LuetInstaller) computeInstall(syncedRepos Repositories, cp pkg.Packages
func (l *LuetInstaller) install(syncedRepos Repositories, toInstall map[string]ArtifactMatch, p pkg.Packages, solution solver.PackagesAssertions, allRepos pkg.PackageDatabase, s *System) error {
// Install packages into rootfs in parallel.
if err := l.download(syncedRepos, toInstall); err != nil {
return errors.Wrap(err, "Downloading packages")
}
all := make(chan ArtifactMatch)
var wg = new(sync.WaitGroup)
// Download first
for i := 0; i < l.Options.Concurrency; i++ {
wg.Add(1)
go l.downloadWorker(i, wg, all)
}
for _, c := range toInstall {
all <- c
}
close(all)
wg.Wait()
all = make(chan ArtifactMatch)
wg = new(sync.WaitGroup)
wg := new(sync.WaitGroup)
// Do the real install
for i := 0; i < l.Options.Concurrency; i++ {

View File

@@ -76,7 +76,7 @@ func (s *Parallel) noRulesInstalled() bool {
return true
}
func (s *Parallel) buildParallelFormula(formulas []bf.Formula, packages pkg.Packages) (bf.Formula, error) {
func (s *Parallel) buildParallelFormula(db pkg.PackageDatabase, formulas []bf.Formula, packages pkg.Packages) (bf.Formula, error) {
var wg = new(sync.WaitGroup)
var wg2 = new(sync.WaitGroup)
@@ -87,7 +87,7 @@ func (s *Parallel) buildParallelFormula(formulas []bf.Formula, packages pkg.Pack
go func(wg *sync.WaitGroup, c <-chan pkg.Package) {
defer wg.Done()
for p := range c {
solvable, err := p.BuildFormula(s.DefinitionDatabase, s.ParallelDatabase)
solvable, err := p.BuildFormula(db, s.ParallelDatabase)
if err != nil {
panic(err)
}
@@ -126,13 +126,13 @@ func (s *Parallel) BuildInstalled() (bf.Formula, error) {
var packages pkg.Packages
for _, p := range s.Installed() {
packages = append(packages, p)
for _, dep := range p.Related(s.DefinitionDatabase) {
for _, dep := range p.Related(s.InstalledDatabase) {
packages = append(packages, dep)
}
}
return s.buildParallelFormula(formulas, packages)
return s.buildParallelFormula(s.InstalledDatabase, formulas, packages)
}
// BuildWorld builds the formula which olds the requirements from the package definitions
@@ -148,7 +148,7 @@ func (s *Parallel) BuildWorld(includeInstalled bool) (bf.Formula, error) {
//f = bf.And(f, solvable)
formulas = append(formulas, solvable)
}
return s.buildParallelFormula(formulas, s.World())
return s.buildParallelFormula(s.DefinitionDatabase, formulas, s.World())
}
// BuildWorld builds the formula which olds the requirements from the package definitions
@@ -200,7 +200,7 @@ func (s *Parallel) BuildPartialWorld(includeInstalled bool) (bf.Formula, error)
close(results)
wg2.Wait()
return s.buildParallelFormula(formulas, packages)
return s.buildParallelFormula(s.DefinitionDatabase, formulas, packages)
//return s.buildParallelFormula(formulas, s.World())
}
@@ -471,23 +471,22 @@ func (s *Parallel) UpgradeUniverse(dropremoved bool) (pkg.Packages, PackagesAsse
go func(wg *sync.WaitGroup, c <-chan pkg.Package) {
defer wg.Done()
for p := range c {
available, err := universe.FindPackageVersions(p)
if err != nil {
removed = append(removed, p) /// FIXME: Racy
}
if len(available) == 0 {
available, err := s.DefinitionDatabase.FindPackageVersions(p)
if len(available) == 0 || err != nil {
removed = append(removed, p)
continue
}
bestmatch := available.Best(nil)
// Found a better version available
if !bestmatch.Matches(p) {
encodedP, _ := p.Encode(universe)
P := bf.Var(encodedP)
results <- bf.And(bf.Not(P), r)
encodedP, _ = bestmatch.Encode(universe)
P = bf.Var(encodedP)
results <- bf.And(P, r)
oldP, _ := p.Encode(universe)
toreplaceP := bf.Var(oldP)
best, _ := bestmatch.Encode(universe)
toUpgrade := bf.Var(best)
solvablenew, _ := bestmatch.BuildFormula(s.DefinitionDatabase, s.ParallelDatabase)
results <- bf.And(bf.Not(toreplaceP), bf.And(append(solvablenew, toUpgrade)...))
}
}
}(wg, all)
@@ -514,8 +513,7 @@ func (s *Parallel) UpgradeUniverse(dropremoved bool) (pkg.Packages, PackagesAsse
// Treat removed packages from universe as marked for deletion
if dropremoved {
// SAT encode the clauses against the world
for _, p := range removed {
for _, p := range removed.Unique() {
encodedP, err := p.Encode(universe)
if err != nil {
return nil, nil, errors.Wrap(err, "couldn't encode package")
@@ -699,7 +697,7 @@ func (s *Parallel) Uninstall(checkconflicts, full bool, packs ...pkg.Package) (p
}
}
s2 := &Parallel{Concurrency: s.Concurrency, InstalledDatabase: pkg.NewInMemoryDatabase(false), DefinitionDatabase: s.DefinitionDatabase, ParallelDatabase: pkg.NewInMemoryDatabase(false)}
s2 := &Parallel{Concurrency: s.Concurrency, InstalledDatabase: pkg.NewInMemoryDatabase(false), DefinitionDatabase: s.InstalledDatabase, ParallelDatabase: pkg.NewInMemoryDatabase(false)}
s2.SetResolver(s.Resolver)
// Get the requirements to install the candidate
asserts, err := s2.Install(toRemove)

View File

@@ -671,8 +671,9 @@ var _ = Describe("Parallel", func() {
Expect(err).ToNot(HaveOccurred())
Expect(solution).To(ContainElement(A))
Expect(solution).To(ContainElement(B))
Expect(len(solution)).To(Equal(1))
Expect(len(solution)).To(Equal(2))
})
It("Uninstalls complex packages correctly, even if shared deps are required by system packages", func() {
@@ -1285,12 +1286,172 @@ var _ = Describe("Parallel", func() {
Expect(uninstall[0].GetName()).To(Equal("a"))
Expect(uninstall[0].GetVersion()).To(Equal("1.1"))
Expect(solution).To(ContainElement(PackageAssert{Package: A1, Value: true}))
Expect(solution).To(ContainElement(PackageAssert{Package: B, Value: true}))
Expect(solution).To(ContainElement(PackageAssert{Package: C, Value: false}))
Expect(solution).To(ContainElement(PackageAssert{Package: A, Value: false}))
Expect(solution).ToNot(ContainElement(PackageAssert{Package: C, Value: true}))
Expect(solution).ToNot(ContainElement(PackageAssert{Package: A, Value: true}))
Expect(len(solution)).To(Equal(4))
Expect(len(solution)).To(Equal(3))
})
It("UpgradeUniverse upgrades correctly", func() {
D := pkg.NewPackage("d", "1.5", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
D.SetCategory("test")
C = pkg.NewPackage("c", "1.5", []*pkg.DefaultPackage{
&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"},
&pkg.DefaultPackage{Name: "d", Version: ">=1.0", Category: "test"},
}, []*pkg.DefaultPackage{})
C.SetCategory("test")
C1 := pkg.NewPackage("c", "1.6", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
C1.SetCategory("test")
B = pkg.NewPackage("b", "1.0", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{})
B.SetCategory("test")
B1 := pkg.NewPackage("b", "1.1", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "c", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
B1.SetCategory("test")
A = pkg.NewPackage("a", "1.1", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "b", Version: "1.0", Category: "test"}}, []*pkg.DefaultPackage{})
A.SetCategory("test")
A1 = pkg.NewPackage("a", "1.2", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "b", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
A1.SetCategory("test")
for _, p := range []pkg.Package{A1, B, B1, C, C1, D} {
_, err := dbDefinitions.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
for _, p := range []pkg.Package{A, B, D} {
_, err := dbInstalled.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
uninstall, solution, err := s.UpgradeUniverse(false)
Expect(err).ToNot(HaveOccurred())
Expect(len(uninstall)).To(Equal(2))
Expect(uninstall).To(ContainElement(A))
Expect(uninstall).To(ContainElement(B))
Expect(solution).To(ContainElement(PackageAssert{Package: A1, Value: true}))
Expect(solution).To(ContainElement(PackageAssert{Package: B1, Value: true}))
Expect(solution).To(ContainElement(PackageAssert{Package: C1, Value: true}))
Expect(len(solution)).To(Equal(6))
})
It("UpgradeUniverse upgrades correctly", func() {
D := pkg.NewPackage("d", "1.5", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
D.SetCategory("test")
D1 := pkg.NewPackage("d", "1.6", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
D1.SetCategory("test")
C = pkg.NewPackage("c", "1.5", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{})
C.SetCategory("test")
B = pkg.NewPackage("b", "1.0", []*pkg.DefaultPackage{
&pkg.DefaultPackage{Name: "c", Version: ">=1.0", Category: "test"},
}, []*pkg.DefaultPackage{})
B.SetCategory("test")
A = pkg.NewPackage("a", "1.1", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "b", Version: "1.0", Category: "test"}}, []*pkg.DefaultPackage{})
A.SetCategory("test")
for _, p := range []pkg.Package{A, B, C, D, D1} {
_, err := dbDefinitions.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
for _, p := range []pkg.Package{A, B, C, D} {
_, err := dbInstalled.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
uninstall, solution, err := s.UpgradeUniverse(false)
Expect(err).ToNot(HaveOccurred())
Expect(len(uninstall)).To(Equal(1))
Expect(uninstall[0].GetName()).To(Equal("d"))
Expect(uninstall[0].GetVersion()).To(Equal("1.5"))
Expect(solution).To(ContainElement(PackageAssert{Package: D1, Value: true}))
Expect(len(solution)).To(Equal(3))
})
It("Upgrade upgrades correctly", func() {
D := pkg.NewPackage("d", "1.5", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
D.SetCategory("test")
C = pkg.NewPackage("c", "1.5", []*pkg.DefaultPackage{
&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"},
&pkg.DefaultPackage{Name: "d", Version: ">=1.0", Category: "test"},
}, []*pkg.DefaultPackage{})
C.SetCategory("test")
C1 := pkg.NewPackage("c", "1.6", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
C1.SetCategory("test")
B = pkg.NewPackage("b", "1.0", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{})
B.SetCategory("test")
B1 := pkg.NewPackage("b", "1.1", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "c", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
B1.SetCategory("test")
A = pkg.NewPackage("a", "1.1", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "b", Version: "1.0", Category: "test"}}, []*pkg.DefaultPackage{})
A.SetCategory("test")
A1 = pkg.NewPackage("a", "1.2", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "b", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
A1.SetCategory("test")
for _, p := range []pkg.Package{A1, B, B1, C, C1, D} {
_, err := dbDefinitions.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
for _, p := range []pkg.Package{A, B, D} {
_, err := dbInstalled.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
uninstall, solution, err := s.Upgrade(false, false)
Expect(err).ToNot(HaveOccurred())
Expect(len(uninstall)).To(Equal(2))
Expect(uninstall).To(ContainElement(A))
Expect(uninstall).To(ContainElement(B))
Expect(solution).To(ContainElement(PackageAssert{Package: A1, Value: true}))
Expect(solution).To(ContainElement(PackageAssert{Package: B1, Value: true}))
Expect(solution).To(ContainElement(PackageAssert{Package: C1, Value: true}))
Expect(len(solution)).To(Equal(6))
})
It("Upgrade upgrades correctly", func() {
D := pkg.NewPackage("d", "1.5", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
D.SetCategory("test")
D1 := pkg.NewPackage("d", "1.6", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
D1.SetCategory("test")
C = pkg.NewPackage("c", "1.5", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{})
C.SetCategory("test")
B = pkg.NewPackage("b", "1.0", []*pkg.DefaultPackage{
&pkg.DefaultPackage{Name: "c", Version: ">=1.0", Category: "test"},
}, []*pkg.DefaultPackage{})
B.SetCategory("test")
A = pkg.NewPackage("a", "1.1", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "b", Version: "1.0", Category: "test"}}, []*pkg.DefaultPackage{})
A.SetCategory("test")
for _, p := range []pkg.Package{A, B, C, D, D1} {
_, err := dbDefinitions.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
for _, p := range []pkg.Package{A, B, C, D} {
_, err := dbInstalled.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
uninstall, solution, err := s.Upgrade(false, false)
Expect(err).ToNot(HaveOccurred())
Expect(len(uninstall)).To(Equal(1))
Expect(uninstall[0].GetName()).To(Equal("d"))
Expect(uninstall[0].GetVersion()).To(Equal("1.5"))
Expect(solution).To(ContainElement(PackageAssert{Package: D1, Value: true}))
Expect(len(solution)).To(Equal(5))
})
})
})

View File

@@ -134,13 +134,13 @@ func (s *Solver) BuildInstalled() (bf.Formula, error) {
var packages pkg.Packages
for _, p := range s.Installed() {
packages = append(packages, p)
for _, dep := range p.Related(s.DefinitionDatabase) {
for _, dep := range p.Related(s.InstalledDatabase) {
packages = append(packages, dep)
}
}
for _, p := range packages {
solvable, err := p.BuildFormula(s.DefinitionDatabase, s.SolverDatabase)
solvable, err := p.BuildFormula(s.InstalledDatabase, s.SolverDatabase)
if err != nil {
return nil, err
}
@@ -399,6 +399,7 @@ func (s *Solver) UpgradeUniverse(dropremoved bool) (pkg.Packages, PackagesAssert
notUptodate := pkg.Packages{}
removed := pkg.Packages{}
toUpgrade := pkg.Packages{}
replacements := map[pkg.Package]pkg.Package{}
// TODO: this is memory expensive, we need to optimize this
universe := pkg.NewInMemoryDatabase(false)
@@ -411,11 +412,9 @@ func (s *Solver) UpgradeUniverse(dropremoved bool) (pkg.Packages, PackagesAssert
// Grab all the installed ones, see if they are eligible for update
for _, p := range s.Installed() {
available, err := universe.FindPackageVersions(p)
if err != nil {
available, err := s.DefinitionDatabase.FindPackageVersions(p)
if len(available) == 0 || err != nil {
removed = append(removed, p)
}
if len(available) == 0 {
continue
}
@@ -424,6 +423,7 @@ func (s *Solver) UpgradeUniverse(dropremoved bool) (pkg.Packages, PackagesAssert
if !bestmatch.Matches(p) {
notUptodate = append(notUptodate, p)
toUpgrade = append(toUpgrade, bestmatch)
replacements[p] = bestmatch
}
}
@@ -437,28 +437,37 @@ func (s *Solver) UpgradeUniverse(dropremoved bool) (pkg.Packages, PackagesAssert
// Treat removed packages from universe as marked for deletion
if dropremoved {
notUptodate = append(notUptodate, removed...)
// SAT encode the clauses against the world
for _, p := range removed.Unique() {
encodedP, err := p.Encode(universe)
if err != nil {
return nil, nil, errors.Wrap(err, "couldn't encode package")
}
P := bf.Var(encodedP)
formulas = append(formulas, bf.And(bf.Not(P), r))
}
}
// SAT encode the clauses against the world
for _, p := range notUptodate.Unique() {
encodedP, err := p.Encode(universe)
for old, new := range replacements {
oldP, err := old.Encode(universe)
if err != nil {
return nil, nil, errors.Wrap(err, "couldn't encode package")
}
P := bf.Var(encodedP)
formulas = append(formulas, bf.And(bf.Not(P), r))
}
for _, p := range toUpgrade {
encodedP, err := p.Encode(universe)
oldencodedP := bf.Var(oldP)
newP, err := new.Encode(universe)
if err != nil {
return nil, nil, errors.Wrap(err, "couldn't encode package")
}
P := bf.Var(encodedP)
formulas = append(formulas, bf.And(P, r))
newEncodedP := bf.Var(newP)
//solvable, err := old.BuildFormula(s.DefinitionDatabase, s.SolverDatabase)
solvablenew, err := new.BuildFormula(s.DefinitionDatabase, s.SolverDatabase)
formulas = append(formulas, bf.And(bf.Not(oldencodedP), bf.And(append(solvablenew, newEncodedP)...)))
}
//formulas = append(formulas, r)
markedForRemoval := pkg.Packages{}
if len(formulas) == 0 {
@@ -607,7 +616,7 @@ func (s *Solver) Uninstall(checkconflicts, full bool, packs ...pkg.Package) (pkg
}
}
s2 := NewSolver(Options{Type: SingleCoreSimple}, pkg.NewInMemoryDatabase(false), s.DefinitionDatabase, pkg.NewInMemoryDatabase(false))
s2 := NewSolver(Options{Type: SingleCoreSimple}, pkg.NewInMemoryDatabase(false), s.InstalledDatabase, pkg.NewInMemoryDatabase(false))
s2.SetResolver(s.Resolver)
// Get the requirements to install the candidate

View File

@@ -671,8 +671,9 @@ var _ = Describe("Solver", func() {
Expect(err).ToNot(HaveOccurred())
Expect(solution).To(ContainElement(A))
Expect(solution).To(ContainElement(B))
Expect(len(solution)).To(Equal(1))
Expect(len(solution)).To(Equal(2))
})
It("Uninstalls complex packages correctly, even if shared deps are required by system packages", func() {
@@ -1209,6 +1210,8 @@ var _ = Describe("Solver", func() {
})
})
Context("Upgrades", func() {
E := pkg.NewPackage("e", "1.5", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{})
E.SetCategory("test")
C := pkg.NewPackage("c", "1.5", []*pkg.DefaultPackage{&pkg.DefaultPackage{Name: "a", Version: ">=1.0", Category: "test"}}, []*pkg.DefaultPackage{})
C.SetCategory("test")
B := pkg.NewPackage("b", "1.0", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{})
@@ -1296,10 +1299,37 @@ var _ = Describe("Solver", func() {
Expect(solution).To(ContainElement(PackageAssert{Package: A1, Value: true}))
Expect(solution).To(ContainElement(PackageAssert{Package: B, Value: true}))
Expect(solution).To(ContainElement(PackageAssert{Package: C, Value: false}))
Expect(solution).To(ContainElement(PackageAssert{Package: A, Value: false}))
Expect(len(solution)).To(Equal(4))
Expect(solution).ToNot(ContainElement(PackageAssert{Package: C, Value: true}))
Expect(solution).ToNot(ContainElement(PackageAssert{Package: A, Value: true}))
Expect(len(solution)).To(Equal(3))
})
It("Suggests to remove untracked packages", func() {
for _, p := range []pkg.Package{E} {
_, err := dbDefinitions.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
for _, p := range []pkg.Package{A, B, C, E} {
_, err := dbInstalled.CreatePackage(p)
Expect(err).ToNot(HaveOccurred())
}
uninstall, solution, err := s.UpgradeUniverse(true)
Expect(err).ToNot(HaveOccurred())
Expect(len(uninstall)).To(Equal(3))
Expect(uninstall).To(ContainElement(B))
Expect(uninstall).To(ContainElement(A))
Expect(uninstall).To(ContainElement(C))
Expect(solution).To(ContainElement(PackageAssert{Package: C, Value: false}))
Expect(solution).To(ContainElement(PackageAssert{Package: B, Value: false}))
Expect(solution).To(ContainElement(PackageAssert{Package: A, Value: false}))
Expect(len(solution)).To(Equal(3))
})
})
})

View File

@@ -1,6 +1,6 @@
steps:
- tar xvf a-test-1.0.package.* -C ./
- ls -liah /a
- mv a /b
requires:
- name: "a"

View File

@@ -12,7 +12,7 @@ oneTimeTearDown() {
testBuild() {
mkdir $tmpdir/testbuild
luet build --tree "$ROOT_DIR/tests/fixtures/finalizers" --destination $tmpdir/testbuild --compression gzip --all > /dev/null
luet build --tree "$ROOT_DIR/tests/fixtures/finalizers" --destination $tmpdir/testbuild --compression gzip --all
buildst=$?
assertEquals 'builds successfully' "$buildst" "0"
assertTrue 'create package' "[ -e '$tmpdir/testbuild/alpine-seed-1.0.package.tar.gz' ]"

View File

@@ -119,8 +119,8 @@ testUpgrade() {
assertTrue 'package installed new A' "[ -e '$tmpdir/testrootfs/testlatest' ]"
# It does remove C as well, no other package depends on it.
assertContains 'does contain test/c-1.0' "$upgrade" 'test/c-1.0'
assertNotContains 'does not attempt to download test/c-1.0' "$upgrade" 'test/c-1.0 downloaded'
#assertContains 'does contain test/c-1.0' "$upgrade" 'test/c-1.0'
#assertNotContains 'does not attempt to download test/c-1.0' "$upgrade" 'test/c-1.0 downloaded'
}
# Load shUnit2.