-
Notifications
You must be signed in to change notification settings - Fork 128
/
module.go
283 lines (236 loc) · 7.72 KB
/
module.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// Copyright (c) 2021 Apptainer a Series of LF Projects LLC
// For website terms of use, trademark policy, privacy policy and other
// project policies see https://lfprojects.org/policies
// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.
package plugin
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/apptainer/apptainer/internal/pkg/buildcfg"
"github.com/apptainer/apptainer/internal/pkg/util/bin"
"github.com/apptainer/apptainer/pkg/sylog"
"github.com/blang/semver/v4"
)
// ApptainerSource represents the symlink name which will
// point to the Apptainer source directory.
const ApptainerSource = "apptainer_source"
// Module describes a Go module with its corresponding path and version.
type Module struct {
Path string
Version string
}
// String returns the string representation of a module.
func (m Module) String() string {
if m.Version != "" {
return m.Path + " " + m.Version
}
return m.Path
}
// GoMod describes a parsed go.mod file.
type GoMod struct {
Module Module
Go string
Require []Require
Exclude []Module
Replace []Replace
}
// GetReplace returns the replace record for the
// provided module path.
func (g GoMod) GetReplace(path string) *Replace {
for _, r := range g.Replace {
if r.Old.Path == path {
return &r
}
}
return nil
}
// GetRequire returns the require record for the
// provided module path.
func (g GoMod) GetRequire(path string) *Require {
for _, r := range g.Require {
if r.Path == path {
return &r
}
}
return nil
}
// GetExclude returns the exclude record for the
// provided module path.
func (g GoMod) GetExclude(path string) *Module {
for _, e := range g.Exclude {
if e.Path == path {
return &e
}
}
return nil
}
// Require describes a require directive in go.mod files.
type Require struct {
Path string
Version string
Indirect bool
}
// String returns the string representation of a require line.
func (r Require) String() string {
indirect := ""
if r.Indirect {
indirect = " // indirect"
}
if r.Version != "" {
return r.Path + " " + r.Version + indirect
}
return r.Path + indirect
}
// Replace describes a replace directive in go.mod files.
type Replace struct {
Old Module
New Module
}
// String returns the string representation of a replace line.
func (r Replace) String() string {
return r.Old.String() + " => " + r.New.String()
}
// GetModules parses the go.mod file found in directory and returns
// a GoMod instance.
func GetModules(dir string) (*GoMod, error) {
var b bytes.Buffer
var e bytes.Buffer
goMod := filepath.Join(dir, "go.mod")
if _, err := os.Stat(goMod); err != nil {
return nil, fmt.Errorf("while getting information for %s: %s", goMod, err)
}
goPath, err := bin.FindBin("go")
if err != nil {
return nil, fmt.Errorf("while retrieving go command path: %s", err)
}
cmd := exec.Command(goPath, "mod", "edit", "-json", goMod)
cmd.Stdout = &b
cmd.Stderr = &e
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("while reading %s: %s\nCommand error:\n%s", goMod, err, e.String())
}
modules := new(GoMod)
if err := json.NewDecoder(&b).Decode(modules); err != nil {
return nil, fmt.Errorf("while decoding json data: %s", err)
}
return modules, nil
}
// PrepareGoModules returns a byte array containing a generated go.mod matching
// Apptainer modules in use in order to compile/load the plugin with same version
// of dependencies.
func PrepareGoModules(pluginDir string, disableMinorCheck bool) ([]byte, error) {
var goMod bytes.Buffer
singModules, err := GetModules(buildcfg.SOURCEDIR)
if err != nil {
return nil, fmt.Errorf("while getting Apptainer Go modules: %s", err)
}
apptainerPackage := singModules.Module.Path
pluginModules, err := GetModules(pluginDir)
if err != nil {
return nil, fmt.Errorf("while getting plugin Go modules: %s", err)
}
fmt.Fprintf(&goMod, "module %s\n\n", pluginModules.Module.Path)
fmt.Fprintf(&goMod, "go %s\n\n", singModules.Go)
for i, r := range pluginModules.Require {
if i == 0 {
fmt.Fprintf(&goMod, "require (\n")
}
if sr := singModules.GetRequire(r.Path); sr != nil && r.Version != sr.Version {
sylog.Infof("Replacing %q by %q", r, sr)
if err := checkCompatibility(r.Version, sr.Version, disableMinorCheck); err != nil {
return nil, fmt.Errorf("package %q error: %s", r.Path, err)
}
r.Version = sr.Version
} else if r.Path == apptainerPackage {
// force apptainer version to v0.0.0
r.Version = "v0.0.0"
}
if sr := singModules.GetExclude(r.Path); sr != nil && sr.Version == r.Version {
return nil, fmt.Errorf("plugin requires %q but it's excluded by apptainer go.mod %q", r, sr)
}
if sr := singModules.GetReplace(r.Path); sr != nil && sr.New.Version != r.Version {
return nil, fmt.Errorf("plugin requires %q but it's replaced by apptainer go.mod %q", r, sr)
}
fmt.Fprintf(&goMod, "\t%s\n", r)
if i == len(pluginModules.Require)-1 {
fmt.Fprintf(&goMod, ")\n\n")
}
}
fmt.Fprintf(&goMod, "replace (\n")
fmt.Fprintf(&goMod, "\t%s => ./%s\n", apptainerPackage, ApptainerSource)
// inject apptainer replace first
for _, r := range singModules.Replace {
fmt.Fprintf(&goMod, "\t%s\n", r)
}
for _, r := range pluginModules.Replace {
if sr := singModules.GetReplace(r.Old.Path); sr != nil {
if sr.New.Version == r.New.Version && sr.New.Path == r.New.Path {
continue
}
return nil, fmt.Errorf("plugin go.mod contains replace %q while apptainer replaced it with %q", r, sr)
} else if r.Old.Path == apptainerPackage {
// previously added above as first replace
continue
}
if sr := singModules.GetRequire(r.Old.Path); sr != nil {
if r.New.Path != sr.Path {
return nil, fmt.Errorf("plugin go.mod contains replace %q while apptainer requires it with %q", r, sr)
}
}
fmt.Fprintf(&goMod, "\t%s\n", r)
}
fmt.Fprintf(&goMod, ")\n\n")
for i, r := range pluginModules.Exclude {
if i == 0 {
fmt.Fprintf(&goMod, "exclude (\n")
}
// check for version incompatibilities in
// apptainer required and replaced packages
if sr := singModules.GetRequire(r.Path); sr != nil {
if sr.Version != r.Version {
return nil, fmt.Errorf("apptainer go.mod contains require %q incompatible with plugin exclude %q", sr, r)
}
}
if sr := singModules.GetReplace(r.Path); sr != nil {
if sr.New.Version != r.Version {
return nil, fmt.Errorf("apptainer go.mod contains replace %q incompatible with plugin exclude %q", sr, r)
}
}
fmt.Fprintf(&goMod, "\t%s\n", r)
if i == len(pluginModules.Exclude)-1 {
fmt.Fprintf(&goMod, ")\n\n")
}
}
return goMod.Bytes(), nil
}
func checkCompatibility(pv string, sv string, disableMinorCheck bool) error {
pluginVer, err := semver.Make(pv[1:])
if err != nil {
return fmt.Errorf("plugin version %s is not a semantic version: %s", pv, err)
}
apptainerVer, err := semver.Make(sv[1:])
if err != nil {
return fmt.Errorf("apptainer version %s is not a semantic version: %s", sv, err)
}
// if major version doesn't match we abort
if pluginVer.Major != apptainerVer.Major {
return fmt.Errorf("incompatible major version, plugin %s / apptainer %s", pv, sv)
}
// if the plugin package version is > to Apptainer package
// version the backward compatibility is not valid and possible
// failures may occur at compilation, we abort in this case
if !disableMinorCheck && pluginVer.GT(apptainerVer) {
return fmt.Errorf("plugin expect a more recent minor version %s while apptainer uses %s", pv, sv)
}
// at this point we assume that Apptainer
// package version is backward compatible
// with the one used by the plugin
return nil
}