Merge branch 'main' into 21720-material-ui-upgrade
[arvados.git] / cmd / arvados-package / build.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bytes"
9         "context"
10         "fmt"
11         "io"
12         "io/ioutil"
13         "os"
14         "os/exec"
15         "os/user"
16         "path/filepath"
17         "strings"
18
19         "git.arvados.org/arvados.git/lib/crunchrun"
20         "git.arvados.org/arvados.git/sdk/go/ctxlog"
21         "github.com/docker/docker/api/types/container"
22         "github.com/docker/docker/client"
23 )
24
25 func build(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writer) error {
26         if opts.PackageVersion == "" {
27                 var buf bytes.Buffer
28                 cmd := exec.CommandContext(ctx, "bash", "./build/version-at-commit.sh", "HEAD")
29                 cmd.Stdout = &buf
30                 cmd.Stderr = stderr
31                 cmd.Dir = opts.SourceDir
32                 err := cmd.Run()
33                 if err != nil {
34                         return fmt.Errorf("%v: %w", cmd.Args, err)
35                 }
36                 opts.PackageVersion = strings.TrimSpace(buf.String())
37                 ctxlog.FromContext(ctx).Infof("version not specified; using %s", opts.PackageVersion)
38         }
39
40         if opts.PackageChown == "" {
41                 whoami, err := user.Current()
42                 if err != nil {
43                         return fmt.Errorf("user.Current: %w", err)
44                 }
45                 opts.PackageChown = whoami.Uid + ":" + whoami.Gid
46         }
47
48         // Build in a tempdir, then move to the desired destination
49         // dir. Otherwise, errors might cause us to leave a mess:
50         // truncated files, files owned by root, etc.
51         _, prog := filepath.Split(os.Args[0])
52         tmpdir, err := ioutil.TempDir(opts.PackageDir, prog+".")
53         if err != nil {
54                 return err
55         }
56         defer os.RemoveAll(tmpdir)
57         if abs, err := filepath.Abs(tmpdir); err != nil {
58                 return fmt.Errorf("error getting absolute path of tmpdir %s: %w", tmpdir, err)
59         } else {
60                 tmpdir = abs
61         }
62
63         selfbin, err := os.Readlink("/proc/self/exe")
64         if err != nil {
65                 return fmt.Errorf("readlink /proc/self/exe: %w", err)
66         }
67         buildImageName := "arvados-package-build-" + opts.TargetOS
68         packageFilename := "arvados-server-easy_" + opts.PackageVersion + "_amd64.deb"
69
70         if ok, err := dockerImageExists(ctx, buildImageName); err != nil {
71                 return err
72         } else if !ok || opts.RebuildImage {
73                 buildCtrName := strings.Replace(buildImageName, ":", "-", -1)
74                 err = dockerRm(ctx, buildCtrName)
75                 if err != nil {
76                         return err
77                 }
78
79                 defer dockerRm(ctx, buildCtrName)
80                 cmd := exec.CommandContext(ctx, "docker", "run",
81                         "--name", buildCtrName,
82                         "--tmpfs", "/tmp:exec,mode=01777",
83                         "-v", selfbin+":/arvados-package:ro",
84                         "-v", opts.SourceDir+":/arvados:ro",
85                         opts.TargetOS,
86                         "/arvados-package", "_install",
87                         "-eatmydata",
88                         "-type", "package",
89                         "-source", "/arvados",
90                         "-package-version", opts.PackageVersion,
91                 )
92                 cmd.Stdout = stdout
93                 cmd.Stderr = stderr
94                 err = cmd.Run()
95                 if err != nil {
96                         return fmt.Errorf("%v: %w", cmd.Args, err)
97                 }
98
99                 cmd = exec.CommandContext(ctx, "docker", "commit", buildCtrName, buildImageName)
100                 cmd.Stdout = stdout
101                 cmd.Stderr = stderr
102                 err = cmd.Run()
103                 if err != nil {
104                         return fmt.Errorf("docker commit: %w", err)
105                 }
106
107                 ctxlog.FromContext(ctx).Infof("created docker image %s", buildImageName)
108         }
109
110         cmd := exec.CommandContext(ctx, "docker", "run",
111                 "--rm",
112                 "--tmpfs", "/tmp:exec,mode=01777",
113                 "-v", tmpdir+":/pkg",
114                 "-v", selfbin+":/arvados-package:ro",
115                 "-v", opts.SourceDir+":/arvados:ro",
116                 buildImageName,
117                 "eatmydata", "/arvados-package", "_fpm",
118                 "-source", "/arvados",
119                 "-package-version", opts.PackageVersion,
120                 "-package-dir", "/pkg",
121                 "-package-chown", opts.PackageChown,
122                 "-package-maintainer", opts.Maintainer,
123                 "-package-vendor", opts.Vendor,
124         )
125         cmd.Stdout = stdout
126         cmd.Stderr = stderr
127         err = cmd.Run()
128         if err != nil {
129                 return fmt.Errorf("%v: %w", cmd.Args, err)
130         }
131
132         err = os.Rename(tmpdir+"/"+packageFilename, opts.PackageDir+"/"+packageFilename)
133         if err != nil {
134                 return err
135         }
136         cmd = exec.CommandContext(ctx, "ls", "-l", opts.PackageDir+"/"+packageFilename)
137         cmd.Stdout = stdout
138         cmd.Stderr = stderr
139         err = cmd.Run()
140         if err != nil {
141                 return err
142         }
143
144         return nil
145 }
146
147 func dockerRm(ctx context.Context, name string) error {
148         cli, err := client.NewClient(client.DefaultDockerHost, crunchrun.DockerAPIVersion, nil, nil)
149         if err != nil {
150                 return err
151         }
152         ctrs, err := cli.ContainerList(ctx, container.ListOptions{All: true, Limit: -1})
153         if err != nil {
154                 return err
155         }
156         for _, ctr := range ctrs {
157                 for _, ctrname := range ctr.Names {
158                         if ctrname == "/"+name {
159                                 err = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{})
160                                 if err != nil {
161                                         return fmt.Errorf("error removing container %s: %w", ctr.ID, err)
162                                 }
163                                 break
164                         }
165                 }
166         }
167         return nil
168 }