forked from paketo-buildpacks/conda-env-update
-
Notifications
You must be signed in to change notification settings - Fork 0
/
conda_runner.go
177 lines (148 loc) · 4.81 KB
/
conda_runner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package condaenvupdate
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/paketo-buildpacks/packit/v2/fs"
"github.com/paketo-buildpacks/packit/v2/pexec"
"github.com/paketo-buildpacks/packit/v2/scribe"
)
//go:generate faux --interface Executable --output fakes/executable.go
// Executable defines the interface for invoking an executable.
type Executable interface {
Execute(pexec.Execution) error
}
//go:generate faux --interface Summer --output fakes/summer.go
// Summer defines the interface for computing a SHA256 for a set of files
// and/or directories.
type Summer interface {
Sum(arg ...string) (string, error)
}
// CondaRunner implements the Runner interface.
type CondaRunner struct {
logger scribe.Emitter
executable Executable
summer Summer
}
// NewCondaRunner creates an instance of CondaRunner given an Executable, a Summer, and a Logger.
func NewCondaRunner(executable Executable, summer Summer, logger scribe.Emitter) CondaRunner {
return CondaRunner{
executable: executable,
summer: summer,
logger: logger,
}
}
// ShouldRun determines whether the conda environment setup command needs to be
// run, given the path to the app directory and the metadata from the
// preexisting conda-env layer. It returns true if the conda environment setup
// command must be run during this build, the SHA256 of the package-list.txt in
// the app directory, and an error. If there is no package-list.txt, the sha
// returned is an empty string.
func (c CondaRunner) ShouldRun(workingDir string, metadata map[string]interface{}) (run bool, sha string, err error) {
lockfilePath := filepath.Join(workingDir, LockfileName)
_, err = os.Stat(lockfilePath)
if errors.Is(err, os.ErrNotExist) {
return true, "", nil
}
if err != nil {
return false, "", err
}
updatedLockfileSha, err := c.summer.Sum(lockfilePath)
if err != nil {
return false, "", err
}
if updatedLockfileSha == metadata[LockfileShaName] {
return false, updatedLockfileSha, nil
}
return true, updatedLockfileSha, nil
}
// Execute runs the conda environment setup command and cleans up unnecessary
// artifacts. If a vendor directory is present, it uses vendored packages and
// installs them in offline mode. If a packages-list.txt file is present, it creates a
// new environment based on the packages list. Otherwise, it updates the
// existing packages to their latest versions.
//
// For more information about the commands used, see:
// https://docs.conda.io/projects/conda/en/latest/commands/create.html
// https://docs.conda.io/projects/conda/en/latest/commands/update.html
// https://docs.conda.io/projects/conda/en/latest/commands/clean.html
func (c CondaRunner) Execute(condaLayerPath string, condaCachePath string, workingDir string) error {
vendorDirExists, err := fs.Exists(filepath.Join(workingDir, "vendor"))
if err != nil {
return err
}
lockfileExists, err := fs.Exists(filepath.Join(workingDir, LockfileName))
if err != nil {
return err
}
args := []string{
"create",
"--file", filepath.Join(workingDir, LockfileName),
"--prefix", condaLayerPath,
"--yes",
"--quiet",
}
if vendorDirExists {
vendorArgs := []string{
"--channel", filepath.Join(workingDir, "vendor"),
"--override-channels",
"--offline",
}
args = append(args, vendorArgs...)
c.logger.Subprocess("Running conda %s", strings.Join(args, " "))
buffer := bytes.NewBuffer(nil)
err = c.executable.Execute(pexec.Execution{
Args: args,
Stdout: buffer,
Stderr: buffer,
})
if err != nil {
c.logger.Action("Failed to run conda %s", strings.Join(args, " "))
c.logger.Detail(buffer.String())
return fmt.Errorf("failed to run conda command: %w", err)
}
return nil
}
if !lockfileExists {
args = []string{
"env",
"update",
"--prefix", condaLayerPath,
"--file", filepath.Join(workingDir, EnvironmentFileName),
}
}
c.logger.Subprocess("Running CONDA_PKGS_DIRS=%s conda %s", condaCachePath, strings.Join(args, " "))
buffer := bytes.NewBuffer(nil)
err = c.executable.Execute(pexec.Execution{
Args: args,
Env: append(os.Environ(), fmt.Sprintf("CONDA_PKGS_DIRS=%s", condaCachePath)),
Stdout: buffer,
Stderr: buffer,
})
if err != nil {
c.logger.Action("Failed to run CONDA_PKGS_DIRS=%s conda %s", condaCachePath, strings.Join(args, " "))
c.logger.Detail(buffer.String())
return fmt.Errorf("failed to run conda command: %w", err)
}
args = []string{
"clean",
"--packages",
"--tarballs",
}
c.logger.Subprocess("Running conda %s", strings.Join(args, " "))
buffer = bytes.NewBuffer(nil)
err = c.executable.Execute(pexec.Execution{
Args: args,
Stdout: buffer,
Stderr: buffer,
})
if err != nil {
c.logger.Action("Failed to run conda %s", strings.Join(args, " "))
c.logger.Detail(buffer.String())
return fmt.Errorf("failed to run conda command: %w", err)
}
return nil
}