rbx.cp 0.6.0__py3-none-any.whl → 0.7.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.
@@ -3,26 +3,20 @@ import shutil
3
3
  import tempfile
4
4
  from typing import Annotated, Iterable, List, Optional, Sequence, Union
5
5
 
6
- import rich
7
- import rich.prompt
8
6
  import typer
9
- from iso639.language import functools
10
7
 
11
8
  from rbx import console, utils
12
- from rbx.box import cd, git_utils
13
- from rbx.box.environment import get_environment_path
9
+ from rbx.box import cd
14
10
  from rbx.box.presets.fetch import PresetFetchInfo, get_preset_fetch_info
15
11
  from rbx.box.presets.lock_schema import LockedAsset, PresetLock
16
12
  from rbx.box.presets.schema import Preset, TrackedAsset
17
13
  from rbx.config import get_default_app_path
18
- from rbx.grading.judge.digester import digest_cooperatively, digest_file
14
+ from rbx.grading.judge.digester import digest_cooperatively
19
15
 
20
16
  app = typer.Typer(no_args_is_help=True)
21
17
 
22
- LOCAL = 'local'
23
18
 
24
-
25
- def find_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
19
+ def _find_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
26
20
  found = root / 'preset.rbx.yml'
27
21
  if found.exists():
28
22
  return found
@@ -30,7 +24,7 @@ def find_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Pa
30
24
 
31
25
 
32
26
  def get_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Preset:
33
- found = find_preset_yaml(root)
27
+ found = _find_preset_yaml(root)
34
28
  if not found:
35
29
  console.console.print(
36
30
  f'[error][item]preset.rbx.yml[/item] not found in [item]{root.absolute()}[/item][/error]'
@@ -39,7 +33,7 @@ def get_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Preset:
39
33
  return utils.model_from_yaml(Preset, found.read_text())
40
34
 
41
35
 
42
- def find_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
36
+ def _find_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
43
37
  root = root.resolve()
44
38
  problem_yaml_path = root / '.preset-lock.yml'
45
39
  if not problem_yaml_path.is_file():
@@ -47,8 +41,8 @@ def find_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Pa
47
41
  return problem_yaml_path
48
42
 
49
43
 
50
- def get_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[PresetLock]:
51
- found = find_preset_lock(root)
44
+ def _get_preset_lock(root: pathlib.Path = pathlib.Path()) -> Optional[PresetLock]:
45
+ found = _find_preset_lock(root)
52
46
  if not found:
53
47
  return None
54
48
  return utils.model_from_yaml(PresetLock, found.read_text())
@@ -77,34 +71,22 @@ def _find_local_preset(root: pathlib.Path) -> Optional[pathlib.Path]:
77
71
  return problem_yaml_path.parent
78
72
 
79
73
 
80
- def get_preset_installation_path(
81
- name: str, root: pathlib.Path = pathlib.Path()
82
- ) -> pathlib.Path:
83
- if name == LOCAL:
84
- local_path = _find_local_preset(root)
85
- if local_path is None:
86
- console.console.print('[error]Local preset was not found.[/error]')
87
- raise typer.Exit(1)
88
- return local_path
89
- nested_preset_path = _find_nested_preset(root)
90
- if nested_preset_path is not None:
91
- nested_preset = utils.model_from_yaml(
92
- Preset, (nested_preset_path / 'preset.rbx.yml').read_text()
93
- )
94
- if nested_preset.name == name:
95
- return nested_preset_path
96
- return utils.get_app_path() / 'presets' / name
97
-
74
+ def _is_installed_preset(root: pathlib.Path = pathlib.Path()) -> bool:
75
+ preset_path = _find_local_preset(root)
76
+ if preset_path is None:
77
+ return False
78
+ resolved_path = preset_path.resolve()
79
+ return resolved_path.name == '.local.rbx'
98
80
 
99
- def _find_installed_presets() -> List[str]:
100
- folder = utils.get_app_path() / 'presets'
101
81
 
102
- res = []
103
- if get_preset_installation_path('local'):
104
- res.append('local')
105
- for yml in folder.glob('*/preset.rbx.yml'):
106
- res.append(yml.parent.name)
107
- return res
82
+ def _is_active_preset_nested(root: pathlib.Path = pathlib.Path()) -> bool:
83
+ preset_path = _find_local_preset(root)
84
+ if preset_path is None:
85
+ return False
86
+ nested_preset_path = _find_nested_preset(root)
87
+ if nested_preset_path is None:
88
+ return False
89
+ return nested_preset_path == preset_path
108
90
 
109
91
 
110
92
  def _is_contest(root: pathlib.Path = pathlib.Path()) -> bool:
@@ -121,24 +103,6 @@ def _check_is_valid_package(root: pathlib.Path = pathlib.Path()):
121
103
  raise typer.Exit(1)
122
104
 
123
105
 
124
- def _get_preset_package_path(
125
- name: str, is_contest: bool, root: pathlib.Path = pathlib.Path()
126
- ) -> pathlib.Path:
127
- preset_path = get_preset_installation_path(name, root)
128
- preset = get_installed_preset(name, root)
129
-
130
- if is_contest:
131
- assert (
132
- preset.contest is not None
133
- ), 'Preset does not have a contest package definition.'
134
- return preset_path / preset.contest
135
-
136
- assert (
137
- preset.problem is not None
138
- ), 'Preset does not have a problem package definition,'
139
- return preset_path / preset.problem
140
-
141
-
142
106
  def _process_globbing(
143
107
  assets: Iterable[TrackedAsset], preset_dir: pathlib.Path
144
108
  ) -> List[TrackedAsset]:
@@ -154,9 +118,12 @@ def _process_globbing(
154
118
  return res
155
119
 
156
120
 
157
- def _get_preset_tracked_assets(name: str, is_contest: bool) -> List[TrackedAsset]:
158
- preset = get_installed_preset(name)
159
- preset_path = get_preset_installation_path(name)
121
+ def _get_preset_tracked_assets(
122
+ root: pathlib.Path, is_contest: bool
123
+ ) -> List[TrackedAsset]:
124
+ preset = get_active_preset(root)
125
+ preset_path = _find_local_preset(root)
126
+ assert preset_path is not None
160
127
 
161
128
  if is_contest:
162
129
  assert (
@@ -222,7 +189,6 @@ def _find_modified_assets(
222
189
 
223
190
 
224
191
  def _copy_updated_assets(
225
- preset_name: str,
226
192
  preset_lock: PresetLock,
227
193
  is_contest: bool,
228
194
  root: pathlib.Path = pathlib.Path(),
@@ -232,9 +198,11 @@ def _copy_updated_assets(
232
198
  preset_lock.assets, current_package_snapshot
233
199
  )
234
200
 
235
- preset_package_path = _get_preset_package_path(preset_name, is_contest=is_contest)
201
+ preset = get_active_preset(root)
202
+ preset_package_path = _get_active_preset_package_path(root, is_contest)
203
+
236
204
  preset_tracked_assets = _get_preset_tracked_assets(
237
- preset_name, is_contest=is_contest
205
+ preset_package_path, is_contest=is_contest
238
206
  )
239
207
  current_preset_snapshot = _build_package_locked_assets(
240
208
  preset_tracked_assets, preset_package_path
@@ -246,137 +214,151 @@ def _copy_updated_assets(
246
214
  dst_path = root / asset.path
247
215
  shutil.copyfile(str(src_path), str(dst_path))
248
216
  console.console.print(
249
- f'Updated [item]{asset.path}[/item] from preset [item]{preset_name}[/item].'
217
+ f'Updated [item]{asset.path}[/item] from preset [item]{preset.name}[/item].'
250
218
  )
251
219
 
252
220
 
253
- def _try_installing_from_resources(name: str) -> bool:
254
- if name == LOCAL:
255
- return False
256
- rsrc_preset_path = get_default_app_path() / 'presets' / name
257
- if not rsrc_preset_path.exists():
258
- return False
259
- yaml_path = rsrc_preset_path / 'preset.rbx.yml'
260
- if not yaml_path.is_file():
261
- return False
262
- console.console.print(f'Installing preset [item]{name}[/item] from resources...')
263
- _install(rsrc_preset_path, force=True)
264
- return True
265
-
221
+ def get_active_preset_or_null(root: pathlib.Path = pathlib.Path()) -> Optional[Preset]:
222
+ local_preset = _find_local_preset(root)
223
+ if local_preset is not None:
224
+ return get_preset_yaml(local_preset)
225
+ return None
266
226
 
267
- @functools.cache
268
- def _install_from_resources_just_once(name: str) -> bool:
269
- # Send all output to the void since we don't wanna be verbose here.
270
- with console.console.capture():
271
- return _try_installing_from_resources(name)
272
227
 
228
+ def get_active_preset(root: pathlib.Path = pathlib.Path()) -> Preset:
229
+ preset = get_active_preset_or_null(root)
230
+ if preset is None:
231
+ console.console.print('[error]No preset is active.[/error]')
232
+ raise typer.Exit(1)
233
+ return preset
273
234
 
274
- def get_installed_preset_or_null(
275
- name: str, root: pathlib.Path = pathlib.Path()
276
- ) -> Optional[Preset]:
277
- installation_path = get_preset_installation_path(name, root) / 'preset.rbx.yml'
278
- if not installation_path.is_file():
279
- if not _try_installing_from_resources(name):
280
- return None
281
- elif name == 'default':
282
- _install_from_resources_just_once(name)
283
235
 
284
- return utils.model_from_yaml(Preset, installation_path.read_text())
236
+ def get_active_preset_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
237
+ preset_path = _find_local_preset(root)
238
+ if preset_path is None:
239
+ console.console.print('[error]No preset is active.[/error]')
240
+ raise typer.Exit(1)
241
+ return preset_path
285
242
 
286
243
 
287
- def get_installed_preset(name: str, root: pathlib.Path = pathlib.Path()) -> Preset:
288
- preset = get_installed_preset_or_null(name, root)
289
- if preset is None:
244
+ def get_preset_environment_path(
245
+ root: pathlib.Path = pathlib.Path(),
246
+ ) -> Optional[pathlib.Path]:
247
+ preset = get_active_preset_or_null(root)
248
+ if preset is None or preset.env is None:
249
+ return None
250
+ preset_path = get_active_preset_path(root)
251
+ env_path = preset_path / preset.env
252
+ if not env_path.is_file():
290
253
  console.console.print(
291
- f'[error]Preset [item]{name}[/item] is not installed.[/error]'
254
+ f'[error]Preset [item]{preset.name}[/item] environment file [item]{preset.env}[/item] does not exist.[/error]'
292
255
  )
293
256
  raise typer.Exit(1)
294
- return preset
257
+ return env_path
295
258
 
296
259
 
297
- def optionally_install_environment_from_preset(
298
- preset: Preset, root: pathlib.Path = pathlib.Path()
299
- ):
300
- if preset.env is None:
301
- return
302
- env_path = get_environment_path(preset.name)
303
- preset_env_path = root / preset.env
304
- if env_path.is_file():
305
- if digest_file(preset_env_path) == digest_file(env_path):
306
- return
307
- import questionary
308
-
309
- overwrite = questionary.confirm(
310
- 'Preset environment file has changed. Overwrite?',
311
- default=False,
312
- ).ask()
313
- if not overwrite:
314
- return
260
+ def _get_active_preset_package_path(
261
+ root: pathlib.Path = pathlib.Path(),
262
+ is_contest: bool = False,
263
+ ) -> pathlib.Path:
264
+ preset = get_active_preset(root)
265
+ preset_path = _find_local_preset(root)
266
+ assert preset_path is not None
267
+ if is_contest:
268
+ assert (
269
+ preset.contest is not None
270
+ ), 'Preset does not have a contest package definition.'
271
+ return preset_path / preset.contest
272
+ assert (
273
+ preset.problem is not None
274
+ ), 'Preset does not have a problem package definition.'
275
+ return preset_path / preset.problem
276
+
277
+
278
+ def get_preset_fetch_info_with_fallback(
279
+ uri: Optional[str],
280
+ ) -> Optional[PresetFetchInfo]:
281
+ if uri is None:
282
+ # Use active preset if any, otherwise use the default preset.
283
+ if get_active_preset_or_null() is not None:
284
+ return None
285
+ default_preset = get_preset_fetch_info('default')
286
+ if default_preset is None:
287
+ console.console.print(
288
+ '[error]Internal error: could not find [item]default[/item] preset.[/error]'
289
+ )
290
+ raise typer.Exit(1)
291
+ return default_preset
292
+ return get_preset_fetch_info(uri)
293
+
294
+
295
+ def _clean_copied_package_dir(dest: pathlib.Path):
296
+ for box_dir in dest.rglob('.box'):
297
+ shutil.rmtree(str(box_dir), ignore_errors=True)
298
+ for lock in dest.rglob('.preset-lock.yml'):
299
+ lock.unlink(missing_ok=True)
315
300
 
316
- console.console.print(
317
- f'[success]Overwriting the existing environment based on [item]{preset.env}[/item].'
318
- )
319
- env_path.parent.mkdir(parents=True, exist_ok=True)
320
- shutil.copyfile(str(preset_env_path), env_path)
321
301
 
302
+ def _clean_copied_contest_dir(dest: pathlib.Path, delete_local_rbx: bool = True):
303
+ shutil.rmtree(str(dest / 'build'), ignore_errors=True)
304
+ if delete_local_rbx:
305
+ shutil.rmtree(str(dest / '.local.rbx'), ignore_errors=True)
306
+ _clean_copied_package_dir(dest)
322
307
 
323
- def _install(root: pathlib.Path = pathlib.Path(), force: bool = False):
324
- preset = get_preset_yaml(root)
325
308
 
326
- if preset.name == LOCAL:
327
- console.console.print('[error]Naming a preset "local" is prohibited.[/error]')
309
+ def _clean_copied_problem_dir(dest: pathlib.Path):
310
+ shutil.rmtree(str(dest / 'build'), ignore_errors=True)
311
+ _clean_copied_package_dir(dest)
328
312
 
329
- console.console.print(f'Installing preset [item]{preset.name}[/item]...')
330
- installation_path = get_preset_installation_path(preset.name)
331
313
 
332
- if root.resolve().is_relative_to(installation_path.resolve()):
314
+ def _install_preset_from_dir(
315
+ src: pathlib.Path,
316
+ dest: pathlib.Path,
317
+ ensure_contest: bool = False,
318
+ ensure_problem: bool = False,
319
+ update: bool = False,
320
+ override_uri: Optional[str] = None,
321
+ ):
322
+ preset = get_preset_yaml(src)
323
+
324
+ if ensure_contest and preset.contest is None:
333
325
  console.console.print(
334
- '[error]Current folder is nested into the preset installation path, cannot install it.[/error]'
326
+ f'[error]Preset [item]{preset.name}[/item] does not have a contest package definition.[/error]'
335
327
  )
336
328
  raise typer.Exit(1)
337
-
338
- if preset.env is not None:
329
+ if ensure_problem and preset.problem is None:
339
330
  console.console.print(
340
- f'[item]{preset.name}[/item]: Copying environment file...'
331
+ f'[error]Preset [item]{preset.name}[/item] does not have a problem package definition.[/error]'
341
332
  )
342
- should_copy_env = True
343
- if get_environment_path(preset.name).exists():
344
- res = force or rich.prompt.Confirm.ask(
345
- f'Environment [item]{preset.name}[/item] already exists. Overwrite?',
346
- console=console.console,
347
- )
348
- if not res:
349
- should_copy_env = False
350
-
351
- if should_copy_env:
352
- get_environment_path(preset.name).parent.mkdir(parents=True, exist_ok=True)
353
- shutil.rmtree(get_environment_path(preset.name), ignore_errors=True)
354
- shutil.copyfile(str(root / preset.env), get_environment_path(preset.name))
355
-
356
- console.console.print(f'[item]{preset.name}[/item]: Copying preset folder...')
357
- installation_path.parent.mkdir(parents=True, exist_ok=True)
358
- if installation_path.exists():
359
- res = force or rich.prompt.Confirm.ask(
360
- f'Preset [item]{preset.name}[/item] is already installed. Overwrite?',
361
- console=console.console,
362
- )
363
- if not res:
364
- raise typer.Exit(1)
365
- shutil.rmtree(str(installation_path), ignore_errors=True)
366
- copy_tree_normalizing_gitdir(root, installation_path)
367
- shutil.rmtree(str(installation_path / 'build'), ignore_errors=True)
368
- shutil.rmtree(str(installation_path / '.box'), ignore_errors=True)
369
- shutil.rmtree(str(installation_path / '.local.rbx'), ignore_errors=True)
333
+ raise typer.Exit(1)
334
+ dest.parent.mkdir(parents=True, exist_ok=True)
335
+ copy_tree_normalizing_gitdir(src, dest, update=update)
370
336
 
337
+ # Override the uri of the preset.
338
+ if override_uri is not None:
339
+ preset.uri = override_uri
340
+ (dest / 'preset.rbx.yml').write_text(utils.model_to_yaml(preset))
371
341
 
372
- def install_from_local_dir(fetch_info: PresetFetchInfo, force: bool = False) -> str:
373
- pd = pathlib.Path(fetch_info.inner_dir)
374
- preset = get_preset_yaml(pd)
375
- _install(pd, force=force)
376
- return preset.name
342
+ # Clean up all cache and left over directories before copying
343
+ # to avoid conflicts.
344
+ shutil.rmtree(str(dest / 'build'), ignore_errors=True)
345
+ shutil.rmtree(str(dest / '.local.rbx'), ignore_errors=True)
377
346
 
347
+ if preset.contest is not None:
348
+ _clean_copied_contest_dir(dest / preset.contest)
349
+ if preset.problem is not None:
350
+ _clean_copied_problem_dir(dest / preset.problem)
378
351
 
379
- def install_from_remote(fetch_info: PresetFetchInfo, force: bool = False) -> str:
352
+ _clean_copied_package_dir(dest)
353
+
354
+
355
+ def _install_preset_from_remote(
356
+ fetch_info: PresetFetchInfo,
357
+ dest: pathlib.Path,
358
+ ensure_contest: bool = False,
359
+ ensure_problem: bool = False,
360
+ update: bool = False,
361
+ ):
380
362
  import git
381
363
 
382
364
  assert fetch_info.fetch_uri is not None
@@ -387,37 +369,176 @@ def install_from_remote(fetch_info: PresetFetchInfo, force: bool = False) -> str
387
369
  git.Repo.clone_from(fetch_info.fetch_uri, d)
388
370
  pd = pathlib.Path(d)
389
371
  if fetch_info.inner_dir:
390
- pd = pd / fetch_info.inner_dir
391
372
  console.console.print(
392
373
  f'Installing preset from [item]{fetch_info.inner_dir}[/item].'
393
374
  )
394
- preset = get_preset_yaml(pd)
395
- preset.uri = fetch_info.uri
375
+ pd = pd / fetch_info.inner_dir
376
+ _install_preset_from_dir(
377
+ pd,
378
+ dest,
379
+ ensure_contest,
380
+ ensure_problem,
381
+ override_uri=fetch_info.fetch_uri,
382
+ update=update,
383
+ )
396
384
 
397
- (pd / 'preset.rbx.yml').write_text(utils.model_to_yaml(preset))
398
- _install(pd, force=force)
399
- return preset.name
400
385
 
386
+ def _install_preset_from_local_dir(
387
+ fetch_info: PresetFetchInfo,
388
+ dest: pathlib.Path,
389
+ ensure_contest: bool = False,
390
+ ensure_problem: bool = False,
391
+ update: bool = False,
392
+ ):
393
+ pd = pathlib.Path(fetch_info.inner_dir)
394
+ preset = get_preset_yaml(pd)
395
+ console.console.print(
396
+ f'Installing local preset [item]{preset.name}[/item] into [item]{dest}[/item]...'
397
+ )
398
+ _install_preset_from_dir(
399
+ pd,
400
+ dest,
401
+ ensure_contest,
402
+ ensure_problem,
403
+ override_uri=str(pd.resolve()),
404
+ update=update,
405
+ )
401
406
 
402
- def generate_lock(
403
- preset_name: Optional[str] = None, root: pathlib.Path = pathlib.Path()
407
+
408
+ def _install_preset_from_resources(
409
+ fetch_info: PresetFetchInfo,
410
+ dest: pathlib.Path,
411
+ ensure_contest: bool = False,
412
+ ensure_problem: bool = False,
413
+ update: bool = False,
404
414
  ):
405
- if preset_name is None:
406
- preset_lock = get_preset_lock(root)
407
- if preset_lock is None:
408
- console.console.print(
409
- '[error][item].preset-lock.yml[/item] not found. '
410
- 'Specify a preset argument to this function to create a lock from scratch.[/error]'
411
- )
412
- raise typer.Exit(1)
413
- preset_name = preset_lock.preset_name
415
+ rsrc_preset_path = get_default_app_path() / 'presets' / fetch_info.name
416
+ if not rsrc_preset_path.exists():
417
+ return False
418
+ yaml_path = rsrc_preset_path / 'preset.rbx.yml'
419
+ if not yaml_path.is_file():
420
+ return False
421
+ console.console.print(
422
+ f'Installing preset [item]{fetch_info.name}[/item] from resources...'
423
+ )
424
+ _install_preset_from_dir(
425
+ rsrc_preset_path,
426
+ dest,
427
+ ensure_contest,
428
+ ensure_problem,
429
+ override_uri=str(rsrc_preset_path.resolve()),
430
+ update=update,
431
+ )
432
+ return True
433
+
434
+
435
+ def _install_preset_from_fetch_info(
436
+ fetch_info: PresetFetchInfo,
437
+ dest: pathlib.Path,
438
+ ensure_contest: bool = False,
439
+ ensure_problem: bool = False,
440
+ update: bool = False,
441
+ ):
442
+ if fetch_info.is_remote():
443
+ _install_preset_from_remote(
444
+ fetch_info,
445
+ dest,
446
+ ensure_contest=ensure_contest,
447
+ ensure_problem=ensure_problem,
448
+ update=update,
449
+ )
450
+ return
451
+ if fetch_info.is_local_dir():
452
+ _install_preset_from_local_dir(
453
+ fetch_info,
454
+ dest,
455
+ ensure_contest=ensure_contest,
456
+ ensure_problem=ensure_problem,
457
+ update=update,
458
+ )
459
+ return
460
+ if _install_preset_from_resources(
461
+ fetch_info,
462
+ dest,
463
+ ensure_contest=ensure_contest,
464
+ ensure_problem=ensure_problem,
465
+ update=update,
466
+ ):
467
+ return
468
+ console.console.print(
469
+ f'[error]Preset [item]{fetch_info.name}[/item] not found.[/error]'
470
+ )
471
+ raise typer.Exit(1)
472
+
473
+
474
+ def install_preset_at_package(fetch_info: PresetFetchInfo, dest_pkg: pathlib.Path):
475
+ _install_preset_from_fetch_info(fetch_info, dest_pkg / '.local.rbx')
476
+
477
+
478
+ def install_contest(
479
+ dest_pkg: pathlib.Path, fetch_info: Optional[PresetFetchInfo] = None
480
+ ):
481
+ if fetch_info is not None:
482
+ _install_preset_from_fetch_info(
483
+ fetch_info,
484
+ dest_pkg / '.local.rbx',
485
+ ensure_contest=True,
486
+ )
487
+ preset = get_active_preset(dest_pkg)
488
+ preset_path = _find_local_preset(dest_pkg)
489
+ assert preset_path is not None
490
+ if preset.contest is None:
491
+ console.console.print(
492
+ f'[error]Preset [item]{preset.name}[/item] does not have a contest package definition.[/error]'
493
+ )
494
+ raise typer.Exit(1)
495
+
496
+ console.console.print(
497
+ f'Installing contest from [item]{preset_path / preset.contest}[/item] to [item]{dest_pkg}[/item]...'
498
+ )
499
+ shutil.copytree(
500
+ str(preset_path / preset.contest),
501
+ str(dest_pkg),
502
+ dirs_exist_ok=True,
503
+ )
504
+ _clean_copied_contest_dir(dest_pkg, delete_local_rbx=False)
505
+
506
+
507
+ def install_problem(
508
+ dest_pkg: pathlib.Path, fetch_info: Optional[PresetFetchInfo] = None
509
+ ):
510
+ if fetch_info is not None:
511
+ _install_preset_from_fetch_info(
512
+ fetch_info,
513
+ dest_pkg / '.local.rbx',
514
+ ensure_problem=True,
515
+ )
516
+ preset = get_active_preset(dest_pkg)
517
+ preset_path = _find_local_preset(dest_pkg)
518
+ assert preset_path is not None
519
+ if preset.problem is None:
520
+ console.console.print(
521
+ f'[error]Preset [item]{preset.name}[/item] does not have a problem package definition.[/error]'
522
+ )
523
+ raise typer.Exit(1)
524
+
525
+ console.console.print(
526
+ f'Installing problem from [item]{preset_path / preset.problem}[/item] to [item]{dest_pkg}[/item]...'
527
+ )
528
+ shutil.copytree(
529
+ str(preset_path / preset.problem),
530
+ str(dest_pkg),
531
+ dirs_exist_ok=True,
532
+ )
533
+ _clean_copied_problem_dir(dest_pkg)
414
534
 
415
- preset = get_installed_preset(preset_name, root)
416
535
 
417
- tracked_assets = _get_preset_tracked_assets(preset_name, is_contest=_is_contest())
536
+ def generate_lock(root: pathlib.Path = pathlib.Path()):
537
+ preset = get_active_preset(root)
538
+
539
+ tracked_assets = _get_preset_tracked_assets(root, is_contest=_is_contest(root))
418
540
  preset_lock = PresetLock(
419
- name=preset.name if preset_name != LOCAL else LOCAL,
420
- uri=preset.uri,
541
+ name=preset.name,
421
542
  assets=_build_package_locked_assets(tracked_assets, root),
422
543
  )
423
544
 
@@ -428,7 +549,7 @@ def generate_lock(
428
549
 
429
550
 
430
551
  def _sync(try_update: bool = False):
431
- preset_lock = get_preset_lock()
552
+ preset_lock = _get_preset_lock()
432
553
  if preset_lock is None:
433
554
  console.console.print(
434
555
  '[error]Package does not have a [item].preset.lock.yml[/item] file and thus cannot be synced.[/error]'
@@ -438,28 +559,22 @@ def _sync(try_update: bool = False):
438
559
  )
439
560
  raise typer.Exit(1)
440
561
 
441
- should_update = try_update and preset_lock.uri is not None
442
- installed_preset = get_installed_preset_or_null(preset_lock.preset_name)
443
- if installed_preset is None:
444
- if not try_update or preset_lock.uri is None:
445
- console.console.print(
446
- f'[error]Preset [item]{preset_lock.preset_name}[/item] is not installed. Install it before trying to update.'
447
- )
448
- raise typer.Exit(1)
449
- install(preset_lock.uri)
450
- elif should_update:
451
- update(preset_lock.name)
562
+ if try_update:
563
+ update()
452
564
 
453
565
  _copy_updated_assets(
454
- preset_lock.preset_name,
455
566
  preset_lock,
456
567
  is_contest=_is_contest(),
457
568
  )
458
- generate_lock(preset_lock.preset_name)
569
+ generate_lock()
570
+
459
571
 
572
+ def copy_tree_normalizing_gitdir(
573
+ src_path: pathlib.Path, dst_path: pathlib.Path, update: bool = False
574
+ ):
575
+ from rbx.box import git_utils
460
576
 
461
- def copy_tree_normalizing_gitdir(src_path: pathlib.Path, dst_path: pathlib.Path):
462
- shutil.copytree(str(src_path), str(dst_path))
577
+ shutil.copytree(str(src_path), str(dst_path), dirs_exist_ok=update)
463
578
  if not (src_path / '.git').is_file():
464
579
  return
465
580
 
@@ -522,79 +637,42 @@ def copy_local_preset(
522
637
  )
523
638
 
524
639
 
525
- @app.command(
526
- 'install', help='Install preset from current directory or from the given URI.'
527
- )
528
- def install(
529
- uri: Optional[str] = typer.Argument(
530
- None,
531
- help='URI for the preset to install. Might be a Github repository, or even a local path.',
532
- ),
533
- ):
534
- if uri is None:
535
- _install()
640
+ @app.command('update', help='Update preset of current package')
641
+ def update():
642
+ preset = get_active_preset()
643
+ if _is_active_preset_nested():
644
+ console.console.print(
645
+ '[error]Your package is nested inside the active preset. Updating such a preset is not supported.[/error]'
646
+ )
647
+ return
648
+ if not _is_installed_preset():
649
+ console.console.print(
650
+ '[error]Your active preset is not installed in a [item].local.rbx[/item] directory. Updating such a preset is not supported.[/error]'
651
+ )
652
+ return
653
+ if preset.uri is None:
654
+ console.console.print(
655
+ f'[error]Preset [item]{preset.name}[/item] is not updateable because it does not have a remote URI.'
656
+ )
536
657
  return
537
658
 
538
- fetch_info = get_preset_fetch_info(uri)
539
- if fetch_info is None:
540
- console.console.print(f'[error] Preset with URI {uri} not found.[/error]')
541
- raise typer.Exit(1)
542
- if not fetch_info.is_local_dir() and not fetch_info.is_remote():
543
- console.console.print(f'[error]URI {uri} is invalid.[/error]')
544
- raise typer.Exit(1)
545
- if fetch_info.is_remote():
546
- install_from_remote(fetch_info)
547
- else:
548
- install_from_local_dir(fetch_info)
659
+ import questionary
549
660
 
661
+ console.console.print(
662
+ f'Updating preset [item]{preset.name}[/item] from [item]{preset.uri}[/item]...'
663
+ )
664
+ if not questionary.confirm(
665
+ 'Updating local preset from remote will remove all custom changes you made to the preset.',
666
+ default=False,
667
+ ).ask():
668
+ return
550
669
 
551
- @app.command('update', help='Update installed remote presets')
552
- def update(
553
- name: Annotated[
554
- Optional[str], typer.Argument(help='If set, update only this preset.')
555
- ] = None,
556
- ):
557
- if not name:
558
- presets = _find_installed_presets()
559
- else:
560
- presets = [name]
561
-
562
- for preset_name in presets:
563
- if preset_name == LOCAL:
564
- import questionary
565
-
566
- if not questionary.confirm(
567
- 'Updating local preset will remove all custom changes you made to the preset.',
568
- default=False,
569
- ).ask():
570
- continue
571
-
572
- preset = get_installed_preset_or_null(preset_name)
573
- if preset is None:
574
- console.console.print(
575
- f'[error]Preset [item]{preset_name}[/item] is not installed.'
576
- )
577
- continue
578
- if preset.uri is None:
579
- console.console.print(
580
- f'Skipping preset [item]{preset_name}[/item], not remote.'
581
- )
582
- continue
583
- install_from_remote(preset.fetch_info, force=True)
584
-
585
- if preset_name == LOCAL:
586
- # Get global path to the preset.
587
- preset_path = get_preset_installation_path(preset.name)
588
- dest_path = '.local.rbx'
589
- shutil.rmtree(dest_path, ignore_errors=True)
590
- shutil.copytree(preset_path, dest_path)
591
- console.console.print(
592
- '[success]Local preset updated successfully.[/success]'
593
- )
594
- else:
595
- console.console.print(
596
- f'[success]Preset [item]{preset_name}[/item] updated successfully.[/success]'
597
- )
670
+ preset_path = _find_local_preset(pathlib.Path.cwd())
671
+ assert preset_path is not None
672
+ _install_preset_from_fetch_info(preset.fetch_info, dest=preset_path, update=True)
673
+ console.console.print(
674
+ f'[success]Preset [item]{preset.name}[/item] updated successfully.[/success]'
675
+ )
598
676
 
599
677
 
600
678
  @app.command(
@@ -620,16 +698,19 @@ def sync(
620
698
  'lock', help='Generate a lock for this package, based on a existing preset.'
621
699
  )
622
700
  @cd.within_closest_package
623
- def lock(
624
- preset: Annotated[
625
- Optional[str],
626
- typer.Argument(
627
- help='Preset to generate a lock for. If unset, will default to the one in the existing .preset-lock.yml.',
628
- ),
629
- ] = None,
630
- ):
701
+ def lock():
631
702
  _check_is_valid_package()
632
- generate_lock(preset)
703
+ generate_lock()
704
+
705
+
706
+ @app.command('ls', help='List details about the active preset.')
707
+ @cd.within_closest_package
708
+ def ls():
709
+ preset = get_active_preset()
710
+ preset_path = _find_local_preset(pathlib.Path.cwd())
711
+ console.console.print(f'Preset: [item]{preset.name}[/item]')
712
+ console.console.print(f'Path: {preset_path}')
713
+ console.console.print(f'URI: {preset.uri}')
633
714
 
634
715
 
635
716
  @app.callback()