/
LocalRegistry.jl
558 lines (496 loc) · 20.9 KB
/
LocalRegistry.jl
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
"""
# LocalRegistry
Create and maintain local registries for Julia packages. This package
is intended to provide a simple but manual workflow for maintaining
small local registries (private or public) without making any
assumptions about how the registry or the packages are hosted.
Registry creation is done by the function `create_registry`.
Registration of new and updated packages is done by the function `register`.
"""
module LocalRegistry
using RegistryTools: RegistryTools, gitcmd, Compress,
check_and_update_registry_files, ReturnStatus, haserror,
find_registered_version
using UUIDs: uuid4
using Pkg: Pkg, TOML
export create_registry, register
# Note: The `uuid` keyword argument is intentionally omitted from the
# documentation string since it's not intended for users.
"""
create_registry(name, repo)
create_registry(path, repo)
Create a registry with the given `name` or at the local directory
`path`, and with repository URL or path `repo`. The first argument is
interpreted as a path if it has more than one path component and
otherwise as a name. If a path is given, the last path component is
used as the name of the registry. If a name is given, it is created in
the standard registry location. In both cases the registry path must
not previously exist. The repository must be able to be pushed to,
for example by being a bare repository.
*Keyword arguments*
create_registry(...; description = nothing, push = false, gitconfig = Dict())
* `description`: Optional description of the purpose of the registry.
* `push`: If `false`, the registry will only be prepared locally. Review the result and `git push` it manually.
* `gitconfig`: Optional configuration parameters for the `git` command.
"""
function create_registry(name_or_path, repo; description = nothing,
gitconfig::Dict = Dict(), uuid = nothing,
push = false)
if length(splitpath(name_or_path)) > 1
path = abspath(expanduser(name_or_path))
else
path = joinpath(first(DEPOT_PATH), "registries", name_or_path)
end
name = basename(path)
if isempty(name)
# If `path` ends with a slash, `basename` becomes empty and
# the slash needs to be peeled off with `dirname`.
name = basename(dirname(path))
end
if isdir(path) || isfile(path)
error("$path already exists.")
end
mkpath(path)
# The only reason for the `uuid` function argument is to allow
# deterministic testing of the package.
if isnothing(uuid)
uuid = string(uuid4())
end
registry_data = RegistryTools.RegistryData(name, uuid, repo = repo,
description = description)
RegistryTools.write_registry(joinpath(path, "Registry.toml"), registry_data)
git = gitcmd(path, gitconfig)
run(`$git init -q`)
run(`$git add Registry.toml`)
run(`$git commit -qm 'Create registry.'`)
run(`$git remote add origin $repo`)
if push
run(`$git push -u origin master`)
end
@info "Created registry in directory $(path)"
return path
end
"""
register(package; registry = registry)
register(package)
register()
Register `package`. If `package` is omitted, register the package in
the currently active project or in the current directory in case the
active project is not a package.
If `registry` is not specified:
* For registration of a new version, automatically locate the registry
where `package` is available.
* For registration of a new package, fail unless exactly one registry
other than General is installed, in which case that registry is
used.
Notes:
* By default this will, in all cases, `git push` the updated registry
to its remote repository. If you prefer to do the push manually,
use the keyword argument `push = false`.
* The package must live in a git working copy, e.g. having been
cloned by `Pkg.develop`.
`package` can be specified in the following ways:
* By package name. The package must be available in the active `Pkg`
environment.
* By path. This is distinguished from package name by having more than
one path component. A path in the current working directory can be
specified by starting with `"./"`.
* By module. It needs to first be loaded by `using` or `import`.
`registry` can be specified in the following ways:
* By registry name. This must be an exact match to the name of one of
the installed registries.
* By path. This must be an existing local path.
* By URL to its remote location. Everything which doesn't match one of
the previous options is assumed to be a URL.
If `registry` is specified by URL, or the found registry is not a git
clone (i.e. obtained from a package server), a temporary git clone
will be used to perform the registration. In this case `push` must be
`true`.
*Keyword arguments*
register(package; registry = nothing, commit = true, push = true,
branch = nothing, repo = nothing, ignore_reregistration = false,
gitconfig = Dict())
* `registry`: Name, path, or URL of registry.
* `commit`: If `false`, only make the changes to the registry but do not commit. Additionally the registry is allowed to be dirty in the `false` case.
* `push`: If `true`, push the changes to the registry repository automatically. Ignored if `commit` is false.
* `branch`: Branch name to use for the registration.
* `repo`: Specify the package repository explicitly. Otherwise looked up as the `git remote` of the package the first time it is registered.
* `ignore_reregistration`: If `true`, do not raise an error if a version has already been registered (with different content), only an informational message.
* `gitconfig`: Optional configuration parameters for the `git` command.
"""
function register(package::Union{Nothing, Module, AbstractString} = nothing;
registry::Union{Nothing, AbstractString} = nothing,
kwargs...)
do_register(package, registry; kwargs...)
return
end
# Differs from the above by looser type restrictions on `package` and
# `registry`. Also returns false if there was nothing new to register
# and true if something new was registered.
function do_register(package, registry;
commit = true, push = true, branch = nothing,
repo = nothing, ignore_reregistration = false,
gitconfig::Dict = Dict())
# Find and read the `Project.toml` for the package. First look for
# the alternative `JuliaProject.toml`.
package_path = find_package_path(package)
local pkg
for project_file in Base.project_names
pkg = Pkg.Types.read_project(joinpath(package_path, project_file))
if !isnothing(pkg.name)
break
end
end
if isnothing(pkg.name)
error("$(package) does not have a Project.toml or JuliaProject.toml file")
end
# If the package directory is dirty, a different version could be
# present in Project.toml.
if is_dirty(package_path, gitconfig)
error("Package directory is dirty. Stash or commit files.")
end
registry_path = find_registry_path(registry, pkg)
registry_path, is_temporary = check_git_registry(registry_path, gitconfig)
if is_temporary && (!commit || !push)
error("Need to use a temporary git clone of the registry, but commit or push is set to false.")
end
if is_dirty(registry_path, gitconfig)
if commit
error("Registry directory is dirty. Stash or commit files.")
else
@info("Note: registry directory is dirty.")
end
end
# Compute the tree hash for the package and the subdirectory
# location within the git repository. For normal packages living
# at the top level of the repository, `subdir` will be the empty
# string.
tree_hash, subdir = get_tree_hash(package_path, gitconfig)
# Check if this version has already been registered. Note, if it
# was already registered and the contents has changed and
# `ignore_reregistration` is false, this will be caught later.
registered_version = find_registered_version(pkg, registry_path)
if registered_version == tree_hash
@info "This version has already been registered and is unchanged."
return false
elseif !isempty(registered_version) && ignore_reregistration
@info "This version has already been registered. Registration request is ignored. Update the version number to register a new version."
return false
end
# Use the `repo` argument or, if this is a new package
# registration, check for the git remote. If `repo` is `nothing`
# and this is an existing package, the repository information will
# not be updated.
if isnothing(repo)
if !has_package(registry_path, pkg)
package_repo = get_remote_repo(package_path, gitconfig)
else
package_repo = ""
end
else
package_repo = repo
end
@info "Registering package" package_path registry_path package_repo uuid=pkg.uuid version=pkg.version tree_hash subdir
clean_registry = true
git = gitcmd(registry_path, gitconfig)
HEAD = readchomp(`$git rev-parse --verify HEAD`)
saved_branch = readchomp(`$git rev-parse --abbrev-ref HEAD`)
remote = readchomp(`$git remote`)
status = ReturnStatus()
try
check_and_update_registry_files(pkg, package_repo, tree_hash,
registry_path, String[], status,
subdir = subdir)
if !haserror(status)
if commit
if !isnothing(branch)
run(`$git checkout -b $branch`)
end
commit_registry(pkg, package_path, package_repo, tree_hash, git)
if push
if isnothing(branch)
run(`$git push`)
else
run(`$git push --set-upstream $remote $branch`)
end
end
run(`$git checkout $(saved_branch)`)
end
clean_registry = false
end
finally
if clean_registry
run(`$git reset --hard $(HEAD)`)
run(`$git clean -f -d`)
run(`$git checkout $(saved_branch)`)
end
end
# Registration failed. Explain to the user what happened.
if haserror(status)
error(explain_registration_error(status))
end
return true
end
function explain_registration_error(status)
for triggered_check in status.triggered_checks
check = triggered_check.id
data = triggered_check
if check == :version_exists
return "Version $(data.version) has already been registered and the content has changed."
elseif check == :change_package_name
return "Changing package name is not supported."
elseif check == :change_package_uuid
return "Changing package UUID is not allowed."
elseif check == :package_self_dep
return "The package depends on itself."
elseif check == :name_mismatch
return "Error in (Julia)Project.toml: UUID $(data.uuid) refers to package '$(data.reg_name)' in registry but Project.toml has '$(data.project_name)'."
elseif check == :wrong_stdlib_uuid
return "Error in (Julia)Project.toml: UUID $(data.project_uuid) for package $(data.name) should be $(data.stdlib_uuid)"
elseif check == :package_url_missing
return "No repo URL provided for a new package."
elseif check == :unexpected_registration_error
return "Unexpected registration error."
end
end
end
# This does the same thing as `LibGit2.isdirty(LibGit2.GitRepo(path))`
# but also works when `path` is a subdirectory of a git
# repository. Only dirt within the subdirectory is considered.
function is_dirty(path, gitconfig)
git = gitcmd(path, gitconfig)
# TODO: There should be no need for the `-u` option but without it
# a bogus diff is reported in the tests.
return !isempty(read(`$git diff-index -u HEAD -- .`))
end
# If the package is omitted,
# * use the active project if it corresponds to a package,
# * otherwise use the current directory.
function find_package_path(::Nothing)
path = pwd()
if VERSION < v"1.4"
env = Pkg.Types.EnvCache()
if !isnothing(env.pkg)
path = dirname(env.project_file)
end
else
# Pkg.project() was introduced in Julia 1.4 as an experimental
# feature. Effectively this does the same thing as the code
# above but is hopefully more future safe.
project = Pkg.project()
if project.ispackage
path = dirname(project.path)
end
end
return path
end
# If the package is provided as a module, directly find the package
# path from the loaded code. This works both if the module is loaded
# from the current package environment or found in LOAD_PATH.
function find_package_path(package::Module)
return dirname(dirname(pathof(package)))
end
# A string argument is either interpreted as a path or as a package
# name, decided by the number of components returned by `splitpath`.
#
# If the package is given by name, it must be available in the current
# package environment as a developed package.
function find_package_path(package_name::AbstractString)
if length(splitpath(package_name)) > 1
if !isdir(package_name)
error("Package path $(package_name) does not exist.")
end
return abspath(expanduser(package_name))
end
ctx = Pkg.Types.Context()
if !haskey(ctx.env.project.deps, package_name)
error("Unknown package $package_name.")
end
pkg_uuid = ctx.env.project.deps[package_name]
pkg_path = ctx.env.manifest[pkg_uuid].path
if isnothing(pkg_path)
error("Package must be developed to be registered.")
elseif !isabspath(pkg_path)
# If the package is developed with --local, pkg_path is
# relative to the project path.
pkg_path = joinpath(dirname(ctx.env.manifest_file), pkg_path)
end
# `pkg_path` might be a relative path, in which case it is
# relative to the directory of Manifest.toml. If `pkg_path`
# already is an absolute path, this call does not affect it.
pkg_path = abspath(dirname(ctx.env.manifest_file), pkg_path)
return pkg_path
end
function find_registry_path(registry::AbstractString, ::Pkg.Types.Project)
return find_registry_path(registry)
end
function find_registry_path(registry::AbstractString)
# 1. Does `registry` match the name of one of the installed registries?
all_registries = collect_registries()
matching_registries = filter(r -> r.name == registry, all_registries)
if !isempty(matching_registries)
return first(matching_registries).path
end
# 2. Is `registry` an existing path?
path = abspath(expanduser(registry))
if checked_ispath(path)
return path
end
# 3. If not, assume it is a URL.
return registry
end
function find_registry_path(::Nothing, pkg::Pkg.Types.Project)
all_registries = collect_registries()
all_registries_but_general = filter(r -> r.name != "General",
all_registries)
matching_registries = filter(all_registries) do reg_spec
reg_data = Pkg.TOML.parsefile(joinpath(reg_spec.path, "Registry.toml"))
haskey(reg_data["packages"], string(pkg.uuid))
end
if isempty(matching_registries)
if length(all_registries_but_general) == 1
return first(all_registries_but_general).path
else
error("Package $(pkg.name) not found in any registry. Please specify in which registry you want to register it.")
end
elseif length(matching_registries) > 1
error("Package $(pkg.name) is registered in more than one registry, please specify in which you want to register the new version.")
end
return first(matching_registries).path
end
# Starting with Julia 1.4, a registry can either be obtained as a git
# clone of a remote git repository or be downloaded from a Julia
# package server. Starting with Julia 1.7, in the latter case the
# registry can also be stored as a tar archive that hasn't been
# unpacked.
#
# Either way, LocalRegistry must have a git clone to work with, so if
# the registry is not in that form, make a temporary git clone.
function check_git_registry(registry_path_or_url, gitconfig)
if !checked_ispath(registry_path_or_url)
# URL given. Use this to make a git clone.
url = registry_path_or_url
elseif isdir(joinpath(registry_path_or_url, ".git"))
# Path is already a git clone. Nothing left to do.
return registry_path_or_url, false
else
# Registry is given as a path but is not a git clone. Find the
# URL of the registry from Registry.toml.
if VERSION >= v"1.7-"
# This handles both packed and unpacked registries.
try
url = Pkg.Registry.RegistryInstance(registry_path_or_url).repo
catch
error("Bad registry path: $(registry_path_or_url)")
end
else
if looks_like_tar_registry(registry_path_or_url)
error("Non-unpacked registries require Julia 1.7 or later.")
elseif !isdir(registry_path_or_url)
error("Bad registry path: $(registry_path_or_url)")
end
url = Pkg.TOML.parsefile(joinpath(registry_path_or_url, "Registry.toml"))["repo"]
end
end
# Make a temporary clone of the registry at `url`. For Julia 1.3
# and later this will be automatically removed when Julia exits.
# For older Julia it will linger around. Upgrade your Julia if you
# don't like that.
path = mktempdir()
git = gitcmd(path, gitconfig)
try
# Note, the output directory `.` effectively means `path`.
run(`$git clone -q $url .`)
catch
error("Failed to make a temporary git clone of $url")
end
return path, true
end
# On sufficiently old Julia versions (including 1.1 but not 1.5) and
# Windows, `ispath` errors instead of returning false for (at least
# some) invalid paths, like e.g. "file://path".
function checked_ispath(path)
if Sys.iswindows()
try
return ispath(path)
catch
return false
end
end
return ispath(path)
end
function looks_like_tar_registry(path)
endswith(path, ".toml") || return false
isfile(path) || return false
try
return haskey(Pkg.TOML.parsefile(path), "git-tree-sha1")
catch
return false
end
end
# This replaces the use of `Pkg.Types.collect_registries`, which was
# removed in Julia 1.7.
#
# TODO: Once Julia versions before 1.7 are no longer supported,
# consider switching over to use `Pkg.Registry.reachable_registries`
# where this is called.
function collect_registries()
registries = []
for depot in Pkg.depots()
isdir(depot) || continue
reg_dir = joinpath(depot, "registries")
isdir(reg_dir) || continue
for name in readdir(reg_dir)
file = joinpath(reg_dir, name, "Registry.toml")
if isfile(file)
push!(registries, (name = name, path = joinpath(reg_dir, name)))
else
# Packed registry in Julia 1.7+.
file = joinpath(reg_dir, "$(name).toml")
if isfile(file)
push!(registries,
(name = name, path = joinpath(reg_dir,
"$(name).toml")))
end
end
end
end
return registries
end
function has_package(registry_path, pkg::Pkg.Types.Project)
registry = Pkg.TOML.parsefile(joinpath(registry_path, "Registry.toml"))
return haskey(registry["packages"], string(pkg.uuid))
end
function get_tree_hash(package_path, gitconfig)
git = gitcmd(package_path, gitconfig)
subdir = readchomp(`$git rev-parse --show-prefix`)
tree_hash = readchomp(`$git rev-parse HEAD:$subdir`)
# Get rid of trailing slash.
if isempty(basename(subdir))
subdir = dirname(subdir)
end
return tree_hash, subdir
end
function get_remote_repo(package_path, gitconfig)
git = gitcmd(package_path, gitconfig)
remote_names = split(readchomp(`$git remote`), '\n', keepempty=false)
repos = String[]
foreach(remote_names) do remote_name
push!(repos,readchomp(`$git remote get-url $(remote_name)`))
end
length(repos) === 0 && error("No repo URL found. Try calling `register` with the keyword `repo` to provide a URL.")
length(repos) > 1 && error("Multiple repo URLs found. Try calling `register` with the keyword `repo` to provide a URL.\n$(repos)")
return repos[1]
end
function commit_registry(pkg::Pkg.Types.Project, package_path, package_repo, tree_hash, git)
@debug("commit changes")
message = """
New version: $(pkg.name) v$(pkg.version)
UUID: $(pkg.uuid)
Repo: $(package_repo)
Tree: $(string(tree_hash))
"""
run(`$git add --all`)
run(`$git commit -qm $message`)
end
end