2019-11-04 16:16:13 +00:00
// Copyright © 2019 Ettore Di Giacinto <mudler@gentoo.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package compiler
import (
"io/ioutil"
2019-11-10 09:48:07 +00:00
"os"
"path/filepath"
2019-11-11 09:22:55 +00:00
"sync"
2019-11-04 16:16:13 +00:00
2019-11-11 18:19:13 +00:00
. "github.com/mudler/luet/pkg/logger"
2019-11-04 16:16:13 +00:00
"github.com/mudler/luet/pkg/helpers"
pkg "github.com/mudler/luet/pkg/package"
2019-11-11 18:19:13 +00:00
"github.com/mudler/luet/pkg/solver"
2019-11-04 16:16:13 +00:00
"github.com/mudler/luet/pkg/tree"
2019-11-11 09:22:55 +00:00
"github.com/pkg/errors"
2019-11-04 16:16:13 +00:00
)
const BuildFile = "build.yaml"
type LuetCompiler struct {
2019-11-05 16:36:22 +00:00
* tree . CompilerRecipe
2019-11-04 16:16:13 +00:00
Backend CompilerBackend
}
func NewLuetCompiler ( backend CompilerBackend , t pkg . Tree ) Compiler {
2019-11-05 16:36:22 +00:00
// The CompilerRecipe will gives us a tree with only build deps listed.
return & LuetCompiler {
Backend : backend ,
CompilerRecipe : & tree . CompilerRecipe {
tree . Recipe { PackageTree : t } ,
} ,
}
2019-11-04 16:16:13 +00:00
}
2019-11-11 09:22:55 +00:00
func ( cs * LuetCompiler ) compilerWorker ( i int , wg * sync . WaitGroup , cspecs chan CompilationSpec , a * [ ] Artifact , m * sync . Mutex , concurrency int , keepPermissions bool , errors chan error ) {
defer wg . Done ( )
for s := range cspecs {
ar , err := cs . Compile ( concurrency , keepPermissions , s )
if err != nil {
errors <- err
}
m . Lock ( )
* a = append ( * a , ar )
m . Unlock ( )
}
}
func ( cs * LuetCompiler ) CompileParallel ( concurrency int , keepPermissions bool , ps [ ] CompilationSpec ) ( [ ] Artifact , [ ] error ) {
all := make ( chan CompilationSpec )
artifacts := [ ] Artifact { }
mutex := & sync . Mutex { }
errors := make ( chan error , len ( ps ) )
var wg = new ( sync . WaitGroup )
for i := 0 ; i < concurrency ; i ++ {
wg . Add ( 1 )
go cs . compilerWorker ( i , wg , all , & artifacts , mutex , concurrency , keepPermissions , errors )
}
for _ , p := range ps {
all <- p
}
close ( all )
wg . Wait ( )
close ( errors )
var allErrors [ ] error
for e := range errors {
allErrors = append ( allErrors , e )
}
return artifacts , allErrors
}
2019-11-11 18:19:13 +00:00
func ( cs * LuetCompiler ) compileWithImage ( image , buildertaggedImage , packageImage string , concurrency int , keepPermissions bool , p CompilationSpec ) ( Artifact , error ) {
p . SetSeedImage ( image ) // In this case, we ignore the build deps as we suppose that the image has them - otherwise we recompose the tree with a solver,
// and we build all the images first.
keepImg := true
keepPackageImg := true
buildDir := p . Rel ( "build" )
2019-11-05 16:36:22 +00:00
2019-11-11 18:19:13 +00:00
// First we copy the source definitions into the output - we create a copy which the builds will need (we need to cache this phase somehow)
err := helpers . CopyDir ( p . GetPackage ( ) . GetPath ( ) , buildDir )
if err != nil {
return nil , errors . Wrap ( err , "Could not copy package sources" )
2019-11-08 18:57:23 +00:00
2019-11-11 18:19:13 +00:00
}
if buildertaggedImage == "" {
keepImg = false
buildertaggedImage = "luet/" + p . GetPackage ( ) . GetFingerPrint ( ) + "-builder"
}
if packageImage == "" {
keepPackageImg = false
packageImage = "luet/" + p . GetPackage ( ) . GetFingerPrint ( )
}
2019-11-10 09:48:07 +00:00
2019-11-11 18:19:13 +00:00
// First we create the builder image
p . WriteBuildImageDefinition ( filepath . Join ( buildDir , p . GetPackage ( ) . GetFingerPrint ( ) + "-builder.dockerfile" ) )
builderOpts := CompilerBackendOptions {
ImageName : buildertaggedImage ,
SourcePath : buildDir ,
DockerFileName : p . GetPackage ( ) . GetFingerPrint ( ) + "-builder.dockerfile" ,
Destination : p . Rel ( p . GetPackage ( ) . GetFingerPrint ( ) + "-builder.image.tar" ) ,
}
2019-11-10 09:48:07 +00:00
2019-11-11 18:19:13 +00:00
err = cs . Backend . BuildImage ( builderOpts )
if err != nil {
return nil , errors . Wrap ( err , "Could not build image: " + image )
}
2019-11-10 09:48:07 +00:00
2019-11-11 18:19:13 +00:00
err = cs . Backend . ExportImage ( builderOpts )
if err != nil {
return nil , errors . Wrap ( err , "Could not export image" )
}
2019-11-10 09:48:07 +00:00
2019-11-11 18:19:13 +00:00
// Then we write the step image, which uses the builder one
p . WriteStepImageDefinition ( buildertaggedImage , filepath . Join ( buildDir , p . GetPackage ( ) . GetFingerPrint ( ) + ".dockerfile" ) )
runnerOpts := CompilerBackendOptions {
ImageName : packageImage ,
SourcePath : buildDir ,
DockerFileName : p . GetPackage ( ) . GetFingerPrint ( ) + ".dockerfile" ,
Destination : p . Rel ( p . GetPackage ( ) . GetFingerPrint ( ) + ".image.tar" ) ,
}
2019-11-08 18:57:23 +00:00
2019-11-11 18:19:13 +00:00
if ! keepPackageImg {
2019-11-08 18:57:23 +00:00
err = cs . Backend . ImageDefinitionToTar ( runnerOpts )
if err != nil {
2019-11-11 09:22:55 +00:00
return nil , errors . Wrap ( err , "Could not export image to tar" )
2019-11-08 18:57:23 +00:00
}
2019-11-11 18:19:13 +00:00
} else {
if err := cs . Backend . BuildImage ( runnerOpts ) ; err != nil {
return nil , errors . Wrap ( err , "Failed building image" )
}
if err := cs . Backend . ExportImage ( runnerOpts ) ; err != nil {
return nil , errors . Wrap ( err , "Failed exporting image" )
2019-11-10 09:48:07 +00:00
}
2019-11-11 18:19:13 +00:00
}
2019-11-10 09:48:07 +00:00
2019-11-11 18:19:13 +00:00
diffs , err := cs . Backend . Changes ( p . Rel ( p . GetPackage ( ) . GetFingerPrint ( ) + "-builder.image.tar" ) , p . Rel ( p . GetPackage ( ) . GetFingerPrint ( ) + ".image.tar" ) )
if err != nil {
return nil , errors . Wrap ( err , "Could not generate changes from layers" )
}
if ! keepImg {
// We keep them around, so to not reload them from the tar (which should be the "correct way") and we automatically share the same layers
2019-11-08 18:57:23 +00:00
// TODO: Handle caching and optionally do not remove things
err = cs . Backend . RemoveImage ( builderOpts )
if err != nil {
2019-11-11 09:22:55 +00:00
return nil , errors . Wrap ( err , "Could not remove image" )
2019-11-08 18:57:23 +00:00
}
2019-11-11 18:19:13 +00:00
}
rootfs , err := ioutil . TempDir ( p . GetOutputPath ( ) , "rootfs" )
defer os . RemoveAll ( rootfs ) // clean up
2019-11-08 18:57:23 +00:00
2019-11-11 18:19:13 +00:00
// TODO: Compression and such
err = cs . Backend . ExtractRootfs ( CompilerBackendOptions { SourcePath : runnerOpts . Destination , Destination : rootfs } , keepPermissions )
if err != nil {
return nil , errors . Wrap ( err , "Could not extract rootfs" )
}
artifact , err := ExtractArtifactFromDelta ( rootfs , p . Rel ( p . GetPackage ( ) . GetFingerPrint ( ) + ".package.tar" ) , diffs , concurrency , keepPermissions )
if err != nil {
return nil , errors . Wrap ( err , "Could not generate deltas" )
}
2019-11-10 09:48:07 +00:00
2019-11-11 18:19:13 +00:00
return artifact , nil
}
func ( cs * LuetCompiler ) Compile ( concurrency int , keepPermissions bool , p CompilationSpec ) ( Artifact , error ) {
2019-11-10 09:48:07 +00:00
2019-11-11 18:19:13 +00:00
// - If image is not set, we read a base_image. Then we will build one image from it to kick-off our build based
// on how we compute the resolvable tree.
// This means to recursively build all the build-images needed to reach that tree part.
// - We later on compute an hash used to identify the image, so each similar deptree keeps the same build image.
// - If image is set we just generate a plain dockerfile
// Treat last case (easier) first. The image is provided and we just compute a plain dockerfile with the images listed as above
if p . GetImage ( ) != "" {
return cs . compileWithImage ( p . GetImage ( ) , "" , "" , concurrency , keepPermissions , p )
}
// Get build deps tree (ordered)
world , err := cs . Tree ( ) . World ( )
if err != nil {
return nil , errors . Wrap ( err , "While computing tree world" )
}
s := solver . NewSolver ( [ ] pkg . Package { } , world )
solution , err := s . Install ( [ ] pkg . Package { p . GetPackage ( ) } )
if err != nil {
return nil , errors . Wrap ( err , "While computing a solution for " + p . GetPackage ( ) . GetName ( ) )
}
dependencies := solution . Drop ( p . GetPackage ( ) ) . Order ( ) // at this point we should have a flattened list of deps to build, including all of them (with all constraints propagated already)
departifacts := [ ] Artifact { } // TODO: Return this somehow
deperrs := [ ] error { }
var lastHash string
Info ( "Build dependencies:" , dependencies . Explain ( ) )
if len ( dependencies [ 0 ] . Package . GetRequires ( ) ) != 0 {
return nil , errors . New ( "The first dependency of the deptree doesn't have an image base" )
}
for _ , assertion := range dependencies { //highly dependent on the order
if assertion . Value && assertion . Package . Flagged ( ) {
Info ( "Building" , assertion . Package . GetName ( ) )
compileSpec , err := cs . FromPackage ( assertion . Package )
if err != nil {
return nil , errors . New ( "Error while generating compilespec for " + assertion . Package . GetName ( ) )
}
compileSpec . SetOutputPath ( p . GetOutputPath ( ) )
pack , err := cs . Tree ( ) . FindPackage ( p . GetPackage ( ) )
if err != nil {
return nil , errors . Wrap ( err , "While computing a solution for " + p . GetPackage ( ) . GetName ( ) )
}
//TODO: Generate image name of builder image - it should match with the hash up to this point - package
nthsolution , err := s . Install ( [ ] pkg . Package { pack } )
if err != nil {
return nil , errors . Wrap ( err , "While computing a solution for " + p . GetPackage ( ) . GetName ( ) )
}
buildImageHash := "luet/cache-" + nthsolution . Drop ( p . GetPackage ( ) ) . AssertionHash ( )
currentPackageImageHash := "luet/cache-" + nthsolution . AssertionHash ( )
lastHash = currentPackageImageHash
if compileSpec . GetImage ( ) != "" {
artifact , err := cs . compileWithImage ( compileSpec . GetImage ( ) , buildImageHash , currentPackageImageHash , concurrency , keepPermissions , compileSpec )
if err != nil {
deperrs = append ( deperrs , err )
break // stop at first error
}
departifacts = append ( departifacts , artifact )
continue
}
artifact , err := cs . compileWithImage ( buildImageHash , "" , currentPackageImageHash , concurrency , keepPermissions , compileSpec )
if err != nil {
return nil , errors . Wrap ( err , "Failed compiling " + compileSpec . GetPackage ( ) . GetName ( ) )
// deperrs = append(deperrs, err)
// break // stop at first error
}
departifacts = append ( departifacts , artifact )
}
2019-11-05 16:36:22 +00:00
}
2019-11-11 18:19:13 +00:00
return cs . compileWithImage ( lastHash , "" , "" , concurrency , keepPermissions , p )
2019-11-04 16:16:13 +00:00
}
func ( cs * LuetCompiler ) FromPackage ( p pkg . Package ) ( CompilationSpec , error ) {
pack , err := cs . Tree ( ) . GetPackageSet ( ) . FindPackage ( p )
if err != nil {
return nil , err
}
2019-11-10 09:48:07 +00:00
2019-11-04 16:16:13 +00:00
buildFile := pack . Rel ( BuildFile )
if ! helpers . Exists ( buildFile ) {
return nil , errors . New ( "No build file present for " + p . GetFingerPrint ( ) )
}
dat , err := ioutil . ReadFile ( buildFile )
if err != nil {
return nil , err
}
2019-11-10 09:48:07 +00:00
return NewLuetCompilationSpec ( dat , pack )
2019-11-04 16:16:13 +00:00
}
2019-11-04 16:20:00 +00:00
func ( cs * LuetCompiler ) GetBackend ( ) CompilerBackend {
return cs . Backend
}
func ( cs * LuetCompiler ) SetBackend ( b CompilerBackend ) {
cs . Backend = b
}