-
Notifications
You must be signed in to change notification settings - Fork 33
/
Copy pathensure.go
315 lines (273 loc) · 9.34 KB
/
ensure.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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
package pkgcontext
import (
"context"
"os"
"path/filepath"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/pkg/errors"
"gopkg.in/eapache/go-resiliency.v1/retrier"
"github.com/Southclaws/sampctl/pawnpackage"
"github.com/Southclaws/sampctl/print"
"github.com/Southclaws/sampctl/resource"
"github.com/Southclaws/sampctl/run"
"github.com/Southclaws/sampctl/runtime"
"github.com/Southclaws/sampctl/util"
"github.com/Southclaws/sampctl/versioning"
)
// ErrNotRemotePackage describes a repository that does not contain a package definition file
var ErrNotRemotePackage = errors.New("remote repository does not declare a package")
// EnsureDependencies traverses package dependencies and ensures they are up to date
func (pcx *PackageContext) EnsureDependencies(ctx context.Context, forceUpdate bool) (err error) {
if pcx.Package.LocalPath == "" {
return errors.New("package does not represent a locally stored package")
}
if !util.Exists(pcx.Package.LocalPath) {
return errors.New("package local path does not exist")
}
pcx.Package.Vendor = filepath.Join(pcx.Package.LocalPath, "dependencies")
for _, dependency := range pcx.AllDependencies {
dep := dependency
r := retrier.New(retrier.ConstantBackoff(1, 100*time.Millisecond), nil)
err := r.Run(func() error {
print.Verb("attempting to ensure dependency", dep)
errInner := pcx.EnsurePackage(dep, forceUpdate)
if errInner != nil {
print.Warn(errors.Wrapf(errInner, "failed to ensure package %s", dep))
return errInner
}
print.Info(pcx.Package, "successfully ensured dependency files for", dep)
return nil
})
if err != nil {
print.Warn("failed to ensure package", dep, "after 2 attempts, skipping")
continue
}
}
if pcx.Package.Local {
print.Verb(pcx.Package, "package is local, ensuring binaries too")
pcx.ActualRuntime.WorkingDir = pcx.Package.LocalPath
pcx.ActualRuntime.Format = pcx.Package.Format
pcx.ActualRuntime.PluginDeps, err = pcx.GatherPlugins()
if err != nil {
return
}
run.ApplyRuntimeDefaults(&pcx.ActualRuntime)
err = runtime.Ensure(ctx, pcx.GitHub, &pcx.ActualRuntime, false)
if err != nil {
return
}
}
return err
}
// GatherPlugins iterates the AllPlugins list and appends them to the runtime dependencies list
func (pcx *PackageContext) GatherPlugins() (pluginDeps []versioning.DependencyMeta, err error) {
print.Verb(pcx.Package, "gathering", len(pcx.AllPlugins), "plugins from package context")
for _, pluginMeta := range pcx.AllPlugins {
print.Verb("read plugin from dependency:", pluginMeta)
pluginDeps = append(pluginDeps, pluginMeta)
}
print.Verb(pcx.Package, "gathered plugins:", pluginDeps)
return
}
// EnsurePackage will make sure a vendor directory contains the specified package.
// If the package is not present, it will clone it at the correct version tag, sha1 or HEAD
// If the package is present, it will ensure the directory contains the correct version
func (pcx *PackageContext) EnsurePackage(meta versioning.DependencyMeta, forceUpdate bool) error {
var (
dependencyPath = filepath.Join(pcx.Package.Vendor, meta.Repo)
needToClone = false // do we need to clone a new repo?
head *plumbing.Reference
)
repo, err := git.PlainOpen(dependencyPath)
if err != nil && err != git.ErrRepositoryNotExists {
return errors.Wrap(err, "failed to open dependency repository")
} else if err == git.ErrRepositoryNotExists {
print.Verb(meta, "package does not exist at", dependencyPath, "cloning new copy")
needToClone = true
} else {
head, err = repo.Head()
if err != nil {
print.Verb(meta, "package already exists but failed to get repository HEAD:", err)
needToClone = true
err = os.RemoveAll(dependencyPath)
if err != nil {
return errors.Wrap(err, "failed to temporarily remove possibly corrupted dependency repo")
}
} else {
print.Verb(meta, "package already exists at", head)
}
}
if needToClone {
print.Verb(meta, "need to clone new copy from cache")
repo, err = pcx.EnsureDependencyFromCache(meta, dependencyPath, false)
if err != nil {
errInner := os.RemoveAll(dependencyPath)
if errInner != nil {
return errors.Wrap(errInner, "failed to remove corrupted dependency repo")
}
errInner = errors.Wrap(err, "failed to ensure dependency from cache")
if errInner != nil {
return errInner
}
return nil
}
}
print.Verb(meta, "updating dependency package")
err = pcx.updateRepoState(repo, meta, forceUpdate)
if err != nil {
// try once more, but force a pull
print.Verb(meta, "unable to update repo in given state, force-pulling latest from repo tip")
err = pcx.updateRepoState(repo, meta, true)
if err != nil {
return errors.Wrap(err, "failed to update repo state")
}
}
// To install resources (includes from within release archives) we can't use the user's locally
// cloned copy of the package that resides in `dependencies/` because that repository may be
// checked out to a commit that existed before a `pawn.json` file was added that describes where
// resources can be downloaded from. Therefore, we instead instantiate a new pawnpackage.Package from
// the cached version of the package because the cached copy is always at the latest version, or
// at least guaranteed to be either later or equal to the local dependency version.
pkg, err := pawnpackage.GetCachedPackage(meta, pcx.CacheDir)
if err != nil {
return err
}
// But the cached copy will have the latest tag assigned to it, so before ensuring it, apply the
// tag of the actual package we installed.
pkg.Tag = meta.Tag
var includePath string
for _, resource := range pkg.Resources {
if resource.Platform != pcx.Platform {
continue
}
if len(resource.Includes) > 0 {
includePath, err = pcx.extractResourceDependencies(context.Background(), pkg, resource)
if err != nil {
return err
}
pcx.AllIncludePaths = append(pcx.AllIncludePaths, includePath)
}
}
return err
}
func (pcx PackageContext) extractResourceDependencies(
ctx context.Context,
pkg pawnpackage.Package,
res resource.Resource,
) (dir string, err error) {
dir = filepath.Join(pcx.Package.Vendor, res.Path(pkg.Repo))
print.Verb(pkg, "installing resource-based dependency", res.Name, "to", dir)
err = os.MkdirAll(dir, 0700)
if err != nil {
err = errors.Wrap(err, "failed to create target directory")
return
}
_, err = runtime.EnsureVersionedPlugin(
ctx,
pcx.GitHub,
pkg.DependencyMeta,
dir,
pcx.Platform,
res.Version,
pcx.CacheDir,
false,
true,
false,
)
if err != nil {
err = errors.Wrap(err, "failed to ensure asset")
return
}
return dir, nil
}
// updateRepoState takes a repo that exists on disk and ensures it matches tag, branch or commit constraints
func (pcx *PackageContext) updateRepoState(
repo *git.Repository,
meta versioning.DependencyMeta,
forcePull bool,
) (err error) {
print.Verb(meta, "updating repository state with", pcx.GitAuth, "authentication method")
var wt *git.Worktree
if forcePull {
print.Verb(meta, "performing forced pull to latest tip")
repo, err = pcx.EnsureDependencyFromCache(meta, filepath.Join(pcx.Package.Vendor, meta.Repo), true)
if err != nil {
return errors.Wrap(err, "failed to ensure dependency in cache")
}
wt, err = repo.Worktree()
if err != nil {
return errors.Wrap(err, "failed to get repo worktree")
}
err = wt.Pull(&git.PullOptions{
Depth: 1000, // get full history
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return errors.Wrap(err, "failed to force pull for full update")
}
} else {
wt, err = repo.Worktree()
if err != nil {
return errors.Wrap(err, "failed to get repo worktree")
}
}
var (
ref *plumbing.Reference
pullOpts = &git.PullOptions{}
)
if meta.SSH != "" {
pullOpts.Auth = pcx.GitAuth
}
if meta.Tag != "" {
print.Verb(meta, "package has tag constraint:", meta.Tag)
ref, err = versioning.RefFromTag(repo, meta)
if err != nil {
return errors.Wrap(err, "failed to get ref from tag")
}
} else if meta.Branch != "" {
print.Verb(meta, "package has branch constraint:", meta.Branch)
pullOpts.Depth = 1000 // get full history
pullOpts.ReferenceName = plumbing.ReferenceName("refs/heads/" + meta.Branch)
err = wt.Pull(pullOpts)
if err != nil && err != git.NoErrAlreadyUpToDate {
return errors.Wrap(err, "failed to pull repo branch")
}
ref, err = versioning.RefFromBranch(repo, meta)
if err != nil {
return errors.Wrap(err, "failed to get ref from branch")
}
} else if meta.Commit != "" {
pullOpts.Depth = 1000 // get full history
err = wt.Pull(pullOpts)
if err != nil && err != git.NoErrAlreadyUpToDate {
return errors.Wrap(err, "failed to pull repo")
}
ref, err = versioning.RefFromCommit(repo, meta)
if err != nil {
return errors.Wrap(err, "failed to get ref from commit")
}
}
if ref != nil {
print.Verb(meta, "checking out ref determined from constraint:", ref)
err = wt.Checkout(&git.CheckoutOptions{
Hash: ref.Hash(),
Force: true,
})
if err != nil {
return errors.Wrapf(err, "failed to checkout necessary commit %s", ref.Hash())
}
print.Verb(meta, "successfully checked out to", ref.Hash())
} else {
print.Verb(meta, "package does not have version constraint pulling latest")
err = wt.Pull(pullOpts)
if err != nil {
if err == git.NoErrAlreadyUpToDate {
err = nil
} else {
return errors.Wrap(err, "failed to fetch latest package")
}
}
}
return err
}