rbx.cp 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rbx/box/cd.py +2 -2
- rbx/box/cli.py +87 -33
- rbx/box/code.py +133 -84
- rbx/box/contest/build_contest_statements.py +2 -2
- rbx/box/contest/contest_package.py +1 -1
- rbx/box/contest/main.py +29 -2
- rbx/box/environment.py +140 -80
- rbx/box/formatting.py +2 -1
- rbx/box/global_package.py +74 -0
- rbx/box/package.py +11 -24
- rbx/box/packaging/__init__.py +0 -0
- rbx/box/packaging/boca/__init__.py +0 -0
- rbx/box/packaging/polygon/packager.py +3 -3
- rbx/box/presets/__init__.py +369 -53
- rbx/box/presets/lock_schema.py +42 -2
- rbx/box/presets/schema.py +4 -0
- rbx/box/remote.py +21 -2
- rbx/box/retries.py +3 -2
- rbx/box/sanitizers/warning_stack.py +5 -5
- rbx/box/solutions.py +37 -25
- rbx/box/statements/build_statements.py +6 -6
- rbx/box/statements/builders.py +1 -1
- rbx/box/stats.py +10 -0
- rbx/box/stresses.py +47 -66
- rbx/box/stressing/finder_parser.py +11 -16
- rbx/box/tasks.py +33 -22
- rbx/box/testcase_utils.py +3 -3
- rbx/box/tooling/boca/scraper.py +1 -1
- rbx/grading/caching.py +98 -47
- rbx/grading/debug_context.py +31 -0
- rbx/grading/grading_context.py +96 -0
- rbx/grading/judge/cacher.py +93 -21
- rbx/grading/judge/sandbox.py +8 -4
- rbx/grading/judge/sandboxes/isolate.py +3 -2
- rbx/grading/judge/sandboxes/stupid_sandbox.py +3 -2
- rbx/grading/judge/sandboxes/timeit.py +1 -1
- rbx/grading/judge/storage.py +170 -35
- rbx/grading/profiling.py +126 -0
- rbx/grading/steps.py +46 -17
- rbx/grading/steps_with_caching.py +52 -26
- rbx/resources/envs/default.rbx.yml +2 -3
- rbx/resources/envs/isolate.rbx.yml +2 -3
- rbx/resources/presets/default/contest/.gitignore +6 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +14 -1
- rbx/resources/presets/default/contest/statement/contest.rbx.tex +24 -86
- rbx/resources/presets/default/contest/statement/instructions.tex +40 -0
- rbx/resources/presets/default/contest/statement/logo.png +0 -0
- rbx/resources/presets/default/env.rbx.yml +67 -0
- rbx/resources/presets/default/preset.rbx.yml +6 -2
- rbx/resources/presets/default/problem/.gitignore +1 -1
- rbx/resources/presets/default/problem/problem.rbx.yml +12 -8
- rbx/resources/presets/default/shared/contest_template.rbx.tex +57 -0
- rbx/resources/presets/default/shared/icpc.sty +322 -0
- rbx/resources/presets/default/shared/problem_template.rbx.tex +57 -0
- rbx/submitors/codeforces.py +3 -2
- rbx/test.py +1 -1
- rbx/utils.py +6 -1
- {rbx_cp-0.7.0.dist-info → rbx_cp-0.9.0.dist-info}/METADATA +4 -1
- {rbx_cp-0.7.0.dist-info → rbx_cp-0.9.0.dist-info}/RECORD +67 -58
- rbx/resources/presets/default/contest/statement/olymp.sty +0 -250
- rbx/resources/presets/default/contest/statement/template.rbx.tex +0 -42
- rbx/resources/presets/default/problem/statement/olymp.sty +0 -250
- rbx/resources/presets/default/problem/statement/template.rbx.tex +0 -89
- /rbx/resources/presets/default/problem/{gen.cpp → gens/gen.cpp} +0 -0
- /rbx/resources/presets/default/problem/{tests → manual_tests}/samples/000.in +0 -0
- /rbx/resources/presets/default/problem/{tests → manual_tests}/samples/001.in +0 -0
- /rbx/resources/presets/default/problem/{random.py → testplan/random.py} +0 -0
- /rbx/resources/presets/default/problem/{random.txt → testplan/random.txt} +0 -0
- {rbx_cp-0.7.0.dist-info → rbx_cp-0.9.0.dist-info}/LICENSE +0 -0
- {rbx_cp-0.7.0.dist-info → rbx_cp-0.9.0.dist-info}/WHEEL +0 -0
- {rbx_cp-0.7.0.dist-info → rbx_cp-0.9.0.dist-info}/entry_points.txt +0 -0
rbx/box/presets/__init__.py
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
+
import os
|
1
2
|
import pathlib
|
2
3
|
import shutil
|
3
4
|
import tempfile
|
4
|
-
from typing import Annotated, Iterable, List, Optional, Sequence, Union
|
5
|
+
from typing import Annotated, Iterable, List, Optional, Sequence, Set, Tuple, Union
|
5
6
|
|
7
|
+
import ruyaml
|
6
8
|
import typer
|
7
9
|
|
8
10
|
from rbx import console, utils
|
9
11
|
from rbx.box import cd
|
10
12
|
from rbx.box.presets.fetch import PresetFetchInfo, get_preset_fetch_info
|
11
|
-
from rbx.box.presets.lock_schema import LockedAsset, PresetLock
|
13
|
+
from rbx.box.presets.lock_schema import LockedAsset, PresetLock, SymlinkInfo
|
12
14
|
from rbx.box.presets.schema import Preset, TrackedAsset
|
13
15
|
from rbx.config import get_default_app_path
|
14
16
|
from rbx.grading.judge.digester import digest_cooperatively
|
@@ -34,7 +36,7 @@ def get_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Preset:
|
|
34
36
|
|
35
37
|
|
36
38
|
def _find_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
|
37
|
-
root =
|
39
|
+
root = utils.abspath(root)
|
38
40
|
problem_yaml_path = root / '.preset-lock.yml'
|
39
41
|
if not problem_yaml_path.is_file():
|
40
42
|
return None
|
@@ -49,7 +51,7 @@ def _get_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[PresetLock
|
|
49
51
|
|
50
52
|
|
51
53
|
def _find_nested_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
52
|
-
root =
|
54
|
+
root = utils.abspath(root)
|
53
55
|
problem_yaml_path = root / 'preset.rbx.yml'
|
54
56
|
while root != pathlib.PosixPath('/') and not problem_yaml_path.is_file():
|
55
57
|
root = root.parent
|
@@ -61,7 +63,7 @@ def _find_nested_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
|
61
63
|
|
62
64
|
def _find_local_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
|
63
65
|
original_root = root
|
64
|
-
root =
|
66
|
+
root = utils.abspath(root)
|
65
67
|
problem_yaml_path = root / '.local.rbx' / 'preset.rbx.yml'
|
66
68
|
while root != pathlib.PosixPath('/') and not problem_yaml_path.is_file():
|
67
69
|
root = root.parent
|
@@ -75,7 +77,7 @@ def _is_installed_preset(root: pathlib.Path = pathlib.Path()) -> bool:
|
|
75
77
|
preset_path = _find_local_preset(root)
|
76
78
|
if preset_path is None:
|
77
79
|
return False
|
78
|
-
resolved_path =
|
80
|
+
resolved_path = utils.abspath(preset_path)
|
79
81
|
return resolved_path.name == '.local.rbx'
|
80
82
|
|
81
83
|
|
@@ -103,23 +105,68 @@ def _check_is_valid_package(root: pathlib.Path = pathlib.Path()):
|
|
103
105
|
raise typer.Exit(1)
|
104
106
|
|
105
107
|
|
108
|
+
def _glob_while_ignoring(
|
109
|
+
dir: pathlib.Path,
|
110
|
+
glb: str,
|
111
|
+
extra_gitignore: Optional[str] = '.box\nbuild\n',
|
112
|
+
recursive: bool = False,
|
113
|
+
) -> Iterable[pathlib.Path]:
|
114
|
+
from gitignore_parser import parse_gitignore, parse_gitignore_str
|
115
|
+
|
116
|
+
ignore_matchers = []
|
117
|
+
|
118
|
+
if extra_gitignore is not None:
|
119
|
+
ignore_matchers.append(parse_gitignore_str(extra_gitignore, base_dir=dir))
|
120
|
+
|
121
|
+
for file in dir.rglob('.gitignore'):
|
122
|
+
if file.is_file():
|
123
|
+
ignore_matchers.append(parse_gitignore(file))
|
124
|
+
|
125
|
+
def should_ignore(path: pathlib.Path) -> bool:
|
126
|
+
return any(m(str(path)) for m in ignore_matchers)
|
127
|
+
|
128
|
+
for file in dir.rglob(glb) if recursive else dir.glob(glb):
|
129
|
+
if should_ignore(file):
|
130
|
+
continue
|
131
|
+
yield file
|
132
|
+
|
133
|
+
|
106
134
|
def _process_globbing(
|
107
|
-
assets: Iterable[TrackedAsset],
|
135
|
+
assets: Iterable[TrackedAsset], preset_pkg_dir: pathlib.Path
|
108
136
|
) -> List[TrackedAsset]:
|
109
137
|
res = []
|
110
138
|
for asset in assets:
|
111
139
|
if '*' in str(asset.path):
|
112
140
|
glb = str(asset.path)
|
113
|
-
files =
|
114
|
-
|
115
|
-
|
141
|
+
files = _glob_while_ignoring(
|
142
|
+
preset_pkg_dir,
|
143
|
+
glb,
|
144
|
+
)
|
145
|
+
relative_files = [file.relative_to(preset_pkg_dir) for file in files]
|
146
|
+
res.extend(
|
147
|
+
[
|
148
|
+
TrackedAsset(path=path, symlink=asset.symlink)
|
149
|
+
for path in relative_files
|
150
|
+
]
|
151
|
+
)
|
116
152
|
continue
|
117
153
|
res.append(asset)
|
118
154
|
return res
|
119
155
|
|
120
156
|
|
157
|
+
def _dedup_tracked_assets(assets: List[TrackedAsset]) -> List[TrackedAsset]:
|
158
|
+
seen_paths = set()
|
159
|
+
res = []
|
160
|
+
for asset in assets:
|
161
|
+
if asset.path in seen_paths:
|
162
|
+
continue
|
163
|
+
seen_paths.add(asset.path)
|
164
|
+
res.append(asset)
|
165
|
+
return res
|
166
|
+
|
167
|
+
|
121
168
|
def _get_preset_tracked_assets(
|
122
|
-
root: pathlib.Path, is_contest: bool
|
169
|
+
root: pathlib.Path, is_contest: bool, add_symlinks: bool = False
|
123
170
|
) -> List[TrackedAsset]:
|
124
171
|
preset = get_active_preset(root)
|
125
172
|
preset_path = _find_local_preset(root)
|
@@ -129,12 +176,51 @@ def _get_preset_tracked_assets(
|
|
129
176
|
assert (
|
130
177
|
preset.contest is not None
|
131
178
|
), 'Preset does not have a contest package definition.'
|
132
|
-
|
179
|
+
preset_pkg_path = preset_path / preset.contest
|
180
|
+
res = _process_globbing(preset.tracking.contest, preset_pkg_path)
|
181
|
+
else:
|
182
|
+
assert (
|
183
|
+
preset.problem is not None
|
184
|
+
), 'Preset does not have a problem package definition,'
|
185
|
+
preset_pkg_path = preset_path / preset.problem
|
186
|
+
res = _process_globbing(preset.tracking.problem, preset_pkg_path)
|
187
|
+
|
188
|
+
if add_symlinks:
|
189
|
+
for file in _glob_while_ignoring(
|
190
|
+
preset_pkg_path,
|
191
|
+
'*',
|
192
|
+
recursive=True,
|
193
|
+
):
|
194
|
+
if not file.is_symlink() or not file.is_file():
|
195
|
+
continue
|
196
|
+
res.append(
|
197
|
+
TrackedAsset(path=file.relative_to(preset_pkg_path), symlink=True)
|
198
|
+
)
|
133
199
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
200
|
+
return _dedup_tracked_assets(res)
|
201
|
+
|
202
|
+
|
203
|
+
def _get_tracked_assets_symlinks(
|
204
|
+
tracked_assets: List[TrackedAsset],
|
205
|
+
) -> Set[pathlib.Path]:
|
206
|
+
res = set()
|
207
|
+
for asset in tracked_assets:
|
208
|
+
if asset.symlink:
|
209
|
+
res.add(asset.path)
|
210
|
+
return res
|
211
|
+
|
212
|
+
|
213
|
+
def _get_symlink_info(
|
214
|
+
tracked_asset: Union[TrackedAsset, LockedAsset], root: pathlib.Path
|
215
|
+
) -> Optional[SymlinkInfo]:
|
216
|
+
asset_path = root / tracked_asset.path
|
217
|
+
if not asset_path.is_symlink():
|
218
|
+
return None
|
219
|
+
target = pathlib.Path(os.readlink(str(asset_path)))
|
220
|
+
absolute_target = utils.abspath(asset_path.parent / target)
|
221
|
+
is_broken = not absolute_target.exists()
|
222
|
+
is_outside = not absolute_target.is_relative_to(utils.abspath(root))
|
223
|
+
return SymlinkInfo(target=target, is_broken=is_broken, is_outside=is_outside)
|
138
224
|
|
139
225
|
|
140
226
|
def _build_package_locked_assets(
|
@@ -145,10 +231,21 @@ def _build_package_locked_assets(
|
|
145
231
|
for tracked_asset in tracked_assets:
|
146
232
|
asset_path = root / tracked_asset.path
|
147
233
|
if not asset_path.is_file():
|
234
|
+
res.append(
|
235
|
+
LockedAsset(
|
236
|
+
path=tracked_asset.path,
|
237
|
+
hash=None,
|
238
|
+
symlink_info=_get_symlink_info(tracked_asset, root),
|
239
|
+
)
|
240
|
+
)
|
148
241
|
continue
|
149
242
|
with asset_path.open('rb') as f:
|
150
243
|
res.append(
|
151
|
-
LockedAsset(
|
244
|
+
LockedAsset(
|
245
|
+
path=tracked_asset.path,
|
246
|
+
hash=digest_cooperatively(f),
|
247
|
+
symlink_info=_get_symlink_info(tracked_asset, root),
|
248
|
+
)
|
152
249
|
)
|
153
250
|
return res
|
154
251
|
|
@@ -156,14 +253,18 @@ def _build_package_locked_assets(
|
|
156
253
|
def _find_non_modified_assets(
|
157
254
|
reference: List[LockedAsset], current: List[LockedAsset]
|
158
255
|
) -> List[LockedAsset]:
|
159
|
-
|
256
|
+
reference_by_path = {asset.path: asset for asset in reference}
|
160
257
|
|
161
258
|
res = []
|
162
|
-
for asset in
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
259
|
+
for asset in current:
|
260
|
+
# File does not exist in the reference.
|
261
|
+
reference_asset = LockedAsset(path=asset.path, hash=None)
|
262
|
+
|
263
|
+
# File already exists.
|
264
|
+
if asset.path in reference_by_path:
|
265
|
+
reference_asset = reference_by_path[asset.path]
|
266
|
+
|
267
|
+
if asset.was_modified(reference_asset) and not asset.is_broken_symlink():
|
167
268
|
# This is a file that was modified.
|
168
269
|
continue
|
169
270
|
res.append(asset)
|
@@ -173,46 +274,139 @@ def _find_non_modified_assets(
|
|
173
274
|
def _find_modified_assets(
|
174
275
|
reference: List[LockedAsset],
|
175
276
|
current: List[LockedAsset],
|
277
|
+
seen_symlinks: Set[pathlib.Path],
|
176
278
|
):
|
177
|
-
|
279
|
+
current_by_path = {asset.path: asset for asset in current}
|
178
280
|
|
179
281
|
res = []
|
180
|
-
for asset in
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
):
|
185
|
-
#
|
282
|
+
for asset in reference:
|
283
|
+
current_asset = LockedAsset(path=asset.path, hash=None)
|
284
|
+
if asset.path in current_by_path:
|
285
|
+
current_asset = current_by_path[asset.path]
|
286
|
+
if current_asset.path in seen_symlinks and not asset.is_symlink():
|
287
|
+
# TODO: improve this condition, it's almost always triggering
|
288
|
+
# Preset asset should be forced to be a symlink,
|
289
|
+
# but in the current package it is not.
|
290
|
+
res.append(asset)
|
186
291
|
continue
|
187
|
-
|
292
|
+
if current_asset.was_modified(asset, follow_symlinks=True):
|
293
|
+
# This is a file that was modified.
|
294
|
+
res.append(asset)
|
188
295
|
return res
|
189
296
|
|
190
297
|
|
298
|
+
def _copy_preset_file(
|
299
|
+
src: pathlib.Path,
|
300
|
+
dst: pathlib.Path,
|
301
|
+
preset_package_path: pathlib.Path,
|
302
|
+
preset_path: pathlib.Path,
|
303
|
+
force_symlink: bool = False,
|
304
|
+
):
|
305
|
+
if dst.is_file() or dst.is_symlink():
|
306
|
+
dst.unlink(missing_ok=True)
|
307
|
+
if not src.exists():
|
308
|
+
return
|
309
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
310
|
+
if not src.is_symlink() and not force_symlink:
|
311
|
+
shutil.copyfile(str(src), str(dst))
|
312
|
+
return
|
313
|
+
|
314
|
+
# Ensure preset package path is inside the preset path.
|
315
|
+
absolute_preset_package_path = utils.abspath(preset_package_path)
|
316
|
+
absolute_preset_path = utils.abspath(preset_path)
|
317
|
+
assert absolute_preset_package_path.is_relative_to(absolute_preset_path)
|
318
|
+
|
319
|
+
# Get the symlink absolute path.
|
320
|
+
if src.is_symlink():
|
321
|
+
target_relative_path = pathlib.Path(os.readlink(str(src)))
|
322
|
+
target_absolute_path = utils.abspath(src.parent / target_relative_path)
|
323
|
+
|
324
|
+
if target_absolute_path.is_relative_to(absolute_preset_package_path):
|
325
|
+
# The symlink points inside the preset package path.
|
326
|
+
# Copy the symlink as is.
|
327
|
+
dst.symlink_to(target_relative_path)
|
328
|
+
return
|
329
|
+
else:
|
330
|
+
target_absolute_path = utils.abspath(src)
|
331
|
+
|
332
|
+
if not target_absolute_path.is_relative_to(absolute_preset_path):
|
333
|
+
console.console.print(
|
334
|
+
f'[error]Preset [item]{preset_path.name}[/item] has a symlink to [item]{target_absolute_path}[/item] which is outside the preset folder.[/error]'
|
335
|
+
)
|
336
|
+
raise typer.Exit(1)
|
337
|
+
|
338
|
+
# The symlink points somewhere inside the preset folder, fix the symlink.
|
339
|
+
dst_absolute_path = utils.abspath(dst)
|
340
|
+
fixed_target_relative_path = target_absolute_path.relative_to(
|
341
|
+
dst_absolute_path.parent,
|
342
|
+
walk_up=True,
|
343
|
+
)
|
344
|
+
dst.symlink_to(fixed_target_relative_path)
|
345
|
+
|
346
|
+
|
191
347
|
def _copy_updated_assets(
|
192
348
|
preset_lock: PresetLock,
|
193
349
|
is_contest: bool,
|
194
350
|
root: pathlib.Path = pathlib.Path(),
|
351
|
+
force: bool = False,
|
352
|
+
symlinks: bool = False,
|
195
353
|
):
|
196
|
-
|
197
|
-
non_modified_assets = _find_non_modified_assets(
|
198
|
-
preset_lock.assets, current_package_snapshot
|
199
|
-
)
|
200
|
-
|
354
|
+
# Build preset package snapshot.
|
201
355
|
preset = get_active_preset(root)
|
356
|
+
preset_path = get_active_preset_path(root)
|
202
357
|
preset_package_path = _get_active_preset_package_path(root, is_contest)
|
203
358
|
|
204
359
|
preset_tracked_assets = _get_preset_tracked_assets(
|
205
|
-
preset_package_path, is_contest=is_contest
|
360
|
+
preset_package_path, is_contest=is_contest, add_symlinks=symlinks
|
206
361
|
)
|
207
362
|
current_preset_snapshot = _build_package_locked_assets(
|
208
363
|
preset_tracked_assets, preset_package_path
|
209
364
|
)
|
210
|
-
|
365
|
+
|
366
|
+
# Build current package snapshot based on the current preset snapshot.
|
367
|
+
current_package_snapshot = _build_package_locked_assets(current_preset_snapshot)
|
368
|
+
|
369
|
+
non_modified_assets = current_package_snapshot
|
370
|
+
if not force:
|
371
|
+
non_modified_assets = _find_non_modified_assets(
|
372
|
+
preset_lock.assets, current_package_snapshot
|
373
|
+
)
|
374
|
+
|
375
|
+
console.console.print('Tracking the following assets from preset:')
|
376
|
+
for asset in current_preset_snapshot:
|
377
|
+
console.console.print(f' - [item]{asset}[/item]')
|
378
|
+
console.console.print()
|
379
|
+
|
380
|
+
console.console.print('Current package snapshot:')
|
381
|
+
for asset in current_package_snapshot:
|
382
|
+
console.console.print(f' - [item]{asset}[/item]')
|
383
|
+
console.console.print()
|
384
|
+
|
385
|
+
seen_symlinks = _get_tracked_assets_symlinks(preset_tracked_assets)
|
386
|
+
|
387
|
+
assets_to_copy = _find_modified_assets(
|
388
|
+
non_modified_assets, current_preset_snapshot, seen_symlinks
|
389
|
+
)
|
390
|
+
|
391
|
+
# console.console.log(current_package_snapshot)
|
392
|
+
# console.console.log(current_preset_snapshot)
|
393
|
+
|
394
|
+
if not assets_to_copy:
|
395
|
+
console.console.print('[warning]No assets to update.[/warning]')
|
396
|
+
return
|
397
|
+
|
398
|
+
# console.console.log(assets_to_copy)
|
211
399
|
|
212
400
|
for asset in assets_to_copy:
|
213
401
|
src_path = preset_package_path / asset.path
|
214
402
|
dst_path = root / asset.path
|
215
|
-
|
403
|
+
_copy_preset_file(
|
404
|
+
src_path,
|
405
|
+
dst_path,
|
406
|
+
preset_package_path,
|
407
|
+
preset_path,
|
408
|
+
force_symlink=asset.path in seen_symlinks,
|
409
|
+
)
|
216
410
|
console.console.print(
|
217
411
|
f'Updated [item]{asset.path}[/item] from preset [item]{preset.name}[/item].'
|
218
412
|
)
|
@@ -400,7 +594,7 @@ def _install_preset_from_local_dir(
|
|
400
594
|
dest,
|
401
595
|
ensure_contest,
|
402
596
|
ensure_problem,
|
403
|
-
override_uri=str(
|
597
|
+
override_uri=str(utils.abspath(pd)),
|
404
598
|
update=update,
|
405
599
|
)
|
406
600
|
|
@@ -426,7 +620,7 @@ def _install_preset_from_resources(
|
|
426
620
|
dest,
|
427
621
|
ensure_contest,
|
428
622
|
ensure_problem,
|
429
|
-
override_uri=str(
|
623
|
+
override_uri=str(utils.abspath(rsrc_preset_path)),
|
430
624
|
update=update,
|
431
625
|
)
|
432
626
|
return True
|
@@ -475,6 +669,45 @@ def install_preset_at_package(fetch_info: PresetFetchInfo, dest_pkg: pathlib.Pat
|
|
475
669
|
_install_preset_from_fetch_info(fetch_info, dest_pkg / '.local.rbx')
|
476
670
|
|
477
671
|
|
672
|
+
def _install_package_from_preset(
|
673
|
+
preset_path: pathlib.Path,
|
674
|
+
preset_package_inner_path: pathlib.Path,
|
675
|
+
dest_pkg: pathlib.Path,
|
676
|
+
tracked_assets: List[TrackedAsset],
|
677
|
+
):
|
678
|
+
preset_package_path = preset_path / preset_package_inner_path
|
679
|
+
if not preset_package_path.is_dir():
|
680
|
+
console.console.print(
|
681
|
+
f'[error]Preset [item]{preset_path.name}[/item] does not have a [item]{preset_package_inner_path}[/item] package definition.[/error]'
|
682
|
+
)
|
683
|
+
raise typer.Exit(1)
|
684
|
+
|
685
|
+
for file in _glob_while_ignoring(
|
686
|
+
preset_package_path,
|
687
|
+
'*',
|
688
|
+
recursive=True,
|
689
|
+
):
|
690
|
+
if not file.is_file():
|
691
|
+
continue
|
692
|
+
_copy_preset_file(
|
693
|
+
file,
|
694
|
+
dest_pkg / file.relative_to(preset_package_path),
|
695
|
+
preset_package_path,
|
696
|
+
preset_path,
|
697
|
+
)
|
698
|
+
|
699
|
+
for asset in tracked_assets:
|
700
|
+
if not asset.symlink:
|
701
|
+
continue
|
702
|
+
_copy_preset_file(
|
703
|
+
preset_package_path / asset.path,
|
704
|
+
dest_pkg / asset.path,
|
705
|
+
preset_package_path,
|
706
|
+
preset_path,
|
707
|
+
force_symlink=True,
|
708
|
+
)
|
709
|
+
|
710
|
+
|
478
711
|
def install_contest(
|
479
712
|
dest_pkg: pathlib.Path, fetch_info: Optional[PresetFetchInfo] = None
|
480
713
|
):
|
@@ -496,10 +729,8 @@ def install_contest(
|
|
496
729
|
console.console.print(
|
497
730
|
f'Installing contest from [item]{preset_path / preset.contest}[/item] to [item]{dest_pkg}[/item]...'
|
498
731
|
)
|
499
|
-
|
500
|
-
|
501
|
-
str(dest_pkg),
|
502
|
-
dirs_exist_ok=True,
|
732
|
+
_install_package_from_preset(
|
733
|
+
preset_path, preset.contest, dest_pkg, preset.tracking.contest
|
503
734
|
)
|
504
735
|
_clean_copied_contest_dir(dest_pkg, delete_local_rbx=False)
|
505
736
|
|
@@ -525,14 +756,36 @@ def install_problem(
|
|
525
756
|
console.console.print(
|
526
757
|
f'Installing problem from [item]{preset_path / preset.problem}[/item] to [item]{dest_pkg}[/item]...'
|
527
758
|
)
|
528
|
-
|
529
|
-
|
530
|
-
str(dest_pkg),
|
531
|
-
dirs_exist_ok=True,
|
759
|
+
_install_package_from_preset(
|
760
|
+
preset_path, preset.problem, dest_pkg, preset.tracking.problem
|
532
761
|
)
|
533
762
|
_clean_copied_problem_dir(dest_pkg)
|
534
763
|
|
535
764
|
|
765
|
+
def install_preset(
|
766
|
+
dest_pkg: pathlib.Path, fetch_info: Optional[PresetFetchInfo] = None
|
767
|
+
):
|
768
|
+
if fetch_info is None and get_active_preset_or_null() is None:
|
769
|
+
console.console.print(
|
770
|
+
'[error]No preset found to initialize the new preset from.[/error]'
|
771
|
+
)
|
772
|
+
raise typer.Exit(1)
|
773
|
+
if fetch_info is None:
|
774
|
+
_install_preset_from_dir(get_active_preset_path(), dest_pkg)
|
775
|
+
else:
|
776
|
+
_install_preset_from_fetch_info(fetch_info, dest_pkg)
|
777
|
+
|
778
|
+
|
779
|
+
def get_ruyaml(root: pathlib.Path = pathlib.Path()) -> Tuple[ruyaml.YAML, ruyaml.Any]:
|
780
|
+
if not (root / 'preset.rbx.yml').is_file():
|
781
|
+
console.console.print(
|
782
|
+
f'[error]Preset at [item]{root}[/item] does not have a [item]preset.rbx.yml[/item] file.[/error]'
|
783
|
+
)
|
784
|
+
raise typer.Exit(1)
|
785
|
+
res = ruyaml.YAML()
|
786
|
+
return res, res.load(root / 'preset.rbx.yml')
|
787
|
+
|
788
|
+
|
536
789
|
def generate_lock(root: pathlib.Path = pathlib.Path()):
|
537
790
|
preset = get_active_preset(root)
|
538
791
|
|
@@ -548,7 +801,7 @@ def generate_lock(root: pathlib.Path = pathlib.Path()):
|
|
548
801
|
)
|
549
802
|
|
550
803
|
|
551
|
-
def _sync(try_update: bool = False):
|
804
|
+
def _sync(try_update: bool = False, force: bool = False, symlinks: bool = False):
|
552
805
|
preset_lock = _get_preset_lock()
|
553
806
|
if preset_lock is None:
|
554
807
|
console.console.print(
|
@@ -565,6 +818,8 @@ def _sync(try_update: bool = False):
|
|
565
818
|
_copy_updated_assets(
|
566
819
|
preset_lock,
|
567
820
|
is_contest=_is_contest(),
|
821
|
+
force=force,
|
822
|
+
symlinks=symlinks,
|
568
823
|
)
|
569
824
|
generate_lock()
|
570
825
|
|
@@ -621,7 +876,9 @@ def copy_local_preset(
|
|
621
876
|
if not add_submodule:
|
622
877
|
return
|
623
878
|
|
624
|
-
dest_path_rel =
|
879
|
+
dest_path_rel = utils.abspath(dest_path).relative_to(
|
880
|
+
utils.abspath(pathlib.Path.cwd())
|
881
|
+
)
|
625
882
|
path_str = str(dest_path_rel / '.local.rbx')
|
626
883
|
try:
|
627
884
|
current_repo.git.submodule('add', preset_remote_uri, path_str)
|
@@ -637,6 +894,47 @@ def copy_local_preset(
|
|
637
894
|
)
|
638
895
|
|
639
896
|
|
897
|
+
@app.command('create', help='Create a new preset.')
|
898
|
+
def create(
|
899
|
+
name: Annotated[
|
900
|
+
str,
|
901
|
+
typer.Option(
|
902
|
+
help='The name of the preset to create. This will also be the name of the folder.',
|
903
|
+
prompt='What is the name of your new preset?',
|
904
|
+
),
|
905
|
+
],
|
906
|
+
uri: Annotated[
|
907
|
+
str,
|
908
|
+
typer.Option(
|
909
|
+
help='The URI of the new preset.',
|
910
|
+
prompt='What is the URI of your new preset? (ex: rsalesc/rbx-preset for a GitHub repository)',
|
911
|
+
),
|
912
|
+
],
|
913
|
+
from_preset: Annotated[
|
914
|
+
Optional[str],
|
915
|
+
typer.Option(
|
916
|
+
'--preset', '-p', help='The URI of the preset to init the new preset from.'
|
917
|
+
),
|
918
|
+
] = None,
|
919
|
+
):
|
920
|
+
console.console.print(f'Creating new preset [item]{name}[/item]...')
|
921
|
+
|
922
|
+
fetch_info = get_preset_fetch_info_with_fallback(from_preset)
|
923
|
+
dest_path = pathlib.Path(name)
|
924
|
+
if dest_path.exists():
|
925
|
+
console.console.print(
|
926
|
+
f'[error]Directory [item]{dest_path}[/item] already exists.[/error]'
|
927
|
+
)
|
928
|
+
raise typer.Exit(1)
|
929
|
+
|
930
|
+
install_preset(dest_path, fetch_info)
|
931
|
+
|
932
|
+
ru, preset = get_ruyaml(dest_path)
|
933
|
+
preset['name'] = name
|
934
|
+
preset['uri'] = uri
|
935
|
+
utils.save_ruyaml(dest_path / 'preset.rbx.yml', ru, preset)
|
936
|
+
|
937
|
+
|
640
938
|
@app.command('update', help='Update preset of current package')
|
641
939
|
def update():
|
642
940
|
preset = get_active_preset()
|
@@ -689,13 +987,31 @@ def sync(
|
|
689
987
|
help='Whether to fetch an up-to-date version of the installed preset from remote, if available.',
|
690
988
|
),
|
691
989
|
] = False,
|
990
|
+
force: Annotated[
|
991
|
+
bool,
|
992
|
+
typer.Option(
|
993
|
+
'--force',
|
994
|
+
'-f',
|
995
|
+
help='Whether to forcefully overwrite the local assets with the preset assets, even if they have been modified.',
|
996
|
+
),
|
997
|
+
] = False,
|
998
|
+
symlinks: Annotated[
|
999
|
+
bool,
|
1000
|
+
typer.Option(
|
1001
|
+
'--symlinks',
|
1002
|
+
'-s',
|
1003
|
+
help='Whether to update all symlinks in the preset to point to their right targets.',
|
1004
|
+
),
|
1005
|
+
] = False,
|
692
1006
|
):
|
693
1007
|
_check_is_valid_package()
|
694
|
-
_sync(try_update=update)
|
1008
|
+
_sync(try_update=update, force=force, symlinks=symlinks)
|
695
1009
|
|
696
1010
|
|
697
1011
|
@app.command(
|
698
|
-
'lock',
|
1012
|
+
'lock',
|
1013
|
+
help='Generate a lock for this package, based on a existing preset.',
|
1014
|
+
hidden=True,
|
699
1015
|
)
|
700
1016
|
@cd.within_closest_package
|
701
1017
|
def lock():
|
rbx/box/presets/lock_schema.py
CHANGED
@@ -1,12 +1,52 @@
|
|
1
|
-
|
1
|
+
import pathlib
|
2
|
+
from typing import List, Optional
|
2
3
|
|
3
4
|
from pydantic import BaseModel
|
4
5
|
|
5
6
|
from rbx.box.presets.schema import TrackedAsset
|
6
7
|
|
7
8
|
|
9
|
+
class SymlinkInfo(BaseModel):
|
10
|
+
target: pathlib.Path
|
11
|
+
is_broken: bool
|
12
|
+
is_outside: bool
|
13
|
+
|
14
|
+
|
8
15
|
class LockedAsset(TrackedAsset):
|
9
|
-
hash: str
|
16
|
+
hash: Optional[str] = None
|
17
|
+
symlink_info: Optional[SymlinkInfo] = None
|
18
|
+
|
19
|
+
def is_symlink(self) -> bool:
|
20
|
+
return self.symlink_info is not None or self.symlink
|
21
|
+
|
22
|
+
def is_broken_symlink(self) -> bool:
|
23
|
+
return self.symlink_info is not None and self.symlink_info.is_broken
|
24
|
+
|
25
|
+
def was_modified(self, base: 'LockedAsset', follow_symlinks: bool = False) -> bool:
|
26
|
+
if self.is_symlink() != base.is_symlink():
|
27
|
+
return True
|
28
|
+
if self.hash != base.hash and (follow_symlinks or self.symlink_info is None):
|
29
|
+
return True
|
30
|
+
if self.symlink_info is not None and self.symlink_info.is_broken:
|
31
|
+
return True
|
32
|
+
if (
|
33
|
+
self.symlink_info is not None
|
34
|
+
and base.symlink_info is not None
|
35
|
+
and self.symlink_info.target != base.symlink_info.target
|
36
|
+
and not follow_symlinks
|
37
|
+
):
|
38
|
+
return True
|
39
|
+
return False
|
40
|
+
|
41
|
+
def __str__(self) -> str:
|
42
|
+
if self.symlink_info is not None:
|
43
|
+
res = f'{self.path} -> {self.symlink_info.target}'
|
44
|
+
if self.symlink_info.is_broken:
|
45
|
+
res += ' (broken)'
|
46
|
+
if self.symlink_info.is_outside:
|
47
|
+
res += ' (outside)'
|
48
|
+
return res
|
49
|
+
return f'{self.path} ({self.hash})'
|
10
50
|
|
11
51
|
|
12
52
|
class PresetLock(BaseModel):
|
rbx/box/presets/schema.py
CHANGED
@@ -19,6 +19,10 @@ class TrackedAsset(BaseModel):
|
|
19
19
|
# be tracked. Can also be a glob, when specified in the preset config.
|
20
20
|
path: pathlib.Path
|
21
21
|
|
22
|
+
# Whether the asset should be symlinked to the local preset directory,
|
23
|
+
# instead of being copied.
|
24
|
+
symlink: bool = False
|
25
|
+
|
22
26
|
|
23
27
|
class Tracking(BaseModel):
|
24
28
|
# Problem assets that should be tracked and updated by rbx
|