sima-cli 2.1.6__tar.gz → 2.1.7__tar.gz
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.
- {sima_cli-2.1.6/sima_cli.egg-info → sima_cli-2.1.7}/PKG-INFO +1 -1
- {sima_cli-2.1.6 → sima_cli-2.1.7}/pyproject.toml +1 -1
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/__version__.py +1 -1
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/cli.py +50 -5
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/metadata_installer.py +4 -0
- sima_cli-2.1.7/sima_cli/install/package_builder.py +304 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/registry.py +49 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/commands.py +1 -1
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/install.py +181 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/neat.py +37 -6
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/preinstall.py +10 -3
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/utils.py +2 -1
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/netboot.py +126 -24
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/updater.py +11 -16
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/vulcan/artifacts.py +126 -6
- sima_cli-2.1.7/sima_cli/vulcan/commands.py +226 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7/sima_cli.egg-info}/PKG-INFO +1 -1
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/SOURCES.txt +4 -0
- sima_cli-2.1.7/tests/unit/test_firmware.py +60 -0
- sima_cli-2.1.7/tests/unit/test_metadata_installer.py +21 -0
- sima_cli-2.1.7/tests/unit/test_netboot.py +98 -0
- sima_cli-2.1.7/tests/unit/test_package_builder.py +314 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_sdk_image_detection.py +211 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_sdk_preinstall.py +14 -0
- sima_cli-2.1.7/tests/unit/test_vulcan.py +445 -0
- sima_cli-2.1.6/sima_cli/vulcan/commands.py +0 -115
- sima_cli-2.1.6/tests/unit/test_firmware.py +0 -0
- sima_cli-2.1.6/tests/unit/test_vulcan.py +0 -180
- {sima_cli-2.1.6 → sima_cli-2.1.7}/LICENSE +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/MANIFEST.in +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/README.md +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/requirements.txt +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/setup.cfg +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/setup.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/__main__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/app_zoo/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/app_zoo/app.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/app_zoo/commands.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/auth0.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/devportal.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/login.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/oauth.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/data/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/data/resources_internal.yaml +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/data/resources_public.yaml +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/device/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/device/commands.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/mpk/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/mpk/commands.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/discover/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/discover/discover.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/discover/linuxll.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/download/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/download/downloader.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/github_assets.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/hostdriver.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/metadata_info.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/metadata_validator.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/optiview.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/palette.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/mla/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/mla/meminfo.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/model_zoo/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/model_zoo/model.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/network/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/network/network.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/playbooks/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/playbooks/commands.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/playbooks/manager.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/cmdexec.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/config.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/linux_shared_network.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/requirements.json +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/script.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/stop.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/uninstall.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/serial/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/serial/serial.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/storage/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/storage/nvme.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/storage/sdcard.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/bmaptool.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/bootimg.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/cleanlog.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/elxr.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/local.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/query.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/remote.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/upgrade/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/upgrade/selfupdate.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/api_common.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/artifactory.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/common.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/config.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/config_loader.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/container_registries.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/device_api.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/disk.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/docker.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/env.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/errors.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/mpk_api.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/net.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/network.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/pcie.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/pkg_update_check.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/serializers.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/services.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/tag.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/vulcan/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/dependency_links.txt +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/entry_points.txt +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/requires.txt +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/top_level.txt +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/e2e/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/__init__.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_app_zoo.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_auth.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_cli.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_cli_stdio.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_download.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_elxr_update.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_install_stub.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_model_zoo.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_pkg_update_check.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_sdk_uninstall.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_selfupdate.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_skills_commands.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_skills_manager.py +0 -0
- {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_utils.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# sima_cli/__version__.py
|
|
2
|
-
__version__ = "2.1.
|
|
2
|
+
__version__ = "2.1.7"
|
|
@@ -30,6 +30,7 @@ from sima_cli.install.registry import register_packages_commands
|
|
|
30
30
|
from sima_cli.upgrade.selfupdate import register_selfupdate_command
|
|
31
31
|
from sima_cli.playbooks import register_playbook_commands
|
|
32
32
|
from sima_cli.vulcan import register_vulcan_commands
|
|
33
|
+
from sima_cli.vulcan.commands import ENV_BASE_URLS, install_vulcan_package
|
|
33
34
|
|
|
34
35
|
def _configure_stdio_errors() -> None:
|
|
35
36
|
for stream in (getattr(sys, "stdout", None), getattr(sys, "stderr", None)):
|
|
@@ -504,7 +505,31 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
|
|
|
504
505
|
@click.argument("component", required=False)
|
|
505
506
|
@click.option("-v", "--version", help="SDK version (required for SDK-dependent components unless --metadata is provided)")
|
|
506
507
|
@click.option("-m", "--mirror", help="URL to a metadata.json file for generic installation")
|
|
507
|
-
@click.option("-t", "--tag", help="Tag of the package
|
|
508
|
+
@click.option("-t", "--tag", help="Tag of the package. With --vulcan, metadata variant type such as minimum.")
|
|
509
|
+
@click.option("--vulcan", "use_vulcan", is_flag=True, help="Install from Vulcan artifacts using the Vulcan package resolver.")
|
|
510
|
+
@click.option(
|
|
511
|
+
"--env",
|
|
512
|
+
"vulcan_environment",
|
|
513
|
+
type=click.Choice(sorted(ENV_BASE_URLS), case_sensitive=False),
|
|
514
|
+
default=None,
|
|
515
|
+
help="Vulcan artifact environment. Used with --vulcan. Defaults to production.",
|
|
516
|
+
)
|
|
517
|
+
@click.option(
|
|
518
|
+
"--base-url",
|
|
519
|
+
"vulcan_base_url",
|
|
520
|
+
default=None,
|
|
521
|
+
envvar="SIMA_VULCAN_BASE_URL",
|
|
522
|
+
help="Override the Vulcan artifact base URL. Used with --vulcan.",
|
|
523
|
+
)
|
|
524
|
+
@click.option(
|
|
525
|
+
"-d",
|
|
526
|
+
"--install-dir",
|
|
527
|
+
default=".",
|
|
528
|
+
show_default=True,
|
|
529
|
+
type=click.Path(file_okay=False, dir_okay=True, path_type=str),
|
|
530
|
+
help="Directory where Vulcan package resources are downloaded and installed. Used with --vulcan.",
|
|
531
|
+
)
|
|
532
|
+
@click.option("--json", "json_output", is_flag=True, help="With --vulcan, print resolved metadata URL and exit.")
|
|
508
533
|
@click.option(
|
|
509
534
|
"-f",
|
|
510
535
|
"--force",
|
|
@@ -513,7 +538,7 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
|
|
|
513
538
|
help="Force installation even if compatibility checks fail.",
|
|
514
539
|
)
|
|
515
540
|
@click.pass_context
|
|
516
|
-
def install_cmd(ctx, component, version, mirror, tag, force):
|
|
541
|
+
def install_cmd(ctx, component, version, mirror, tag, use_vulcan, vulcan_environment, vulcan_base_url, install_dir, json_output, force):
|
|
517
542
|
"""
|
|
518
543
|
Install SiMa packages.
|
|
519
544
|
|
|
@@ -548,12 +573,29 @@ def install_cmd(ctx, component, version, mirror, tag, force):
|
|
|
548
573
|
"""
|
|
549
574
|
internal = ctx.obj.get("internal", False)
|
|
550
575
|
|
|
576
|
+
if use_vulcan:
|
|
577
|
+
if mirror:
|
|
578
|
+
raise click.ClickException("--mirror cannot be used with --vulcan.")
|
|
579
|
+
if not component:
|
|
580
|
+
raise click.ClickException("You must specify a Vulcan target when using --vulcan.")
|
|
581
|
+
install_vulcan_package(
|
|
582
|
+
target=component,
|
|
583
|
+
environment=vulcan_environment or "production",
|
|
584
|
+
base_url=vulcan_base_url,
|
|
585
|
+
package_type=tag,
|
|
586
|
+
install_dir=install_dir,
|
|
587
|
+
force=force,
|
|
588
|
+
json_output=json_output,
|
|
589
|
+
)
|
|
590
|
+
return None
|
|
591
|
+
|
|
551
592
|
# Metadata-based installation path
|
|
552
593
|
if mirror:
|
|
553
594
|
if component:
|
|
554
595
|
click.echo(f"⚠️ Component '{component}' is ignored when using --metadata. Proceeding with metadata-based installation.")
|
|
555
596
|
click.echo(f"🔧 Installing generic component from metadata URL: {mirror}")
|
|
556
|
-
|
|
597
|
+
install_from_metadata(metadata_url=mirror, internal=internal, force=force)
|
|
598
|
+
return None
|
|
557
599
|
|
|
558
600
|
# No component and no metadata: error
|
|
559
601
|
if not component:
|
|
@@ -562,11 +604,13 @@ def install_cmd(ctx, component, version, mirror, tag, force):
|
|
|
562
604
|
|
|
563
605
|
# if user specified gh: as component, treat it the same as -m
|
|
564
606
|
if component.startswith("gh:"):
|
|
565
|
-
|
|
607
|
+
install_from_metadata(metadata_url=component, internal=False, force=force)
|
|
608
|
+
return None
|
|
566
609
|
|
|
567
610
|
# if the user specified cr: or ghcr: as component, install from container registry
|
|
568
611
|
if component.startswith("cr:") or component.startswith("ghcr:"):
|
|
569
|
-
|
|
612
|
+
install_from_cr(resource_spec=component, internal=internal)
|
|
613
|
+
return None
|
|
570
614
|
|
|
571
615
|
version = resolve_version(version)
|
|
572
616
|
|
|
@@ -601,6 +645,7 @@ def install_cmd(ctx, component, version, mirror, tag, force):
|
|
|
601
645
|
ctx.exit(1)
|
|
602
646
|
|
|
603
647
|
click.echo("✅ Installation complete.")
|
|
648
|
+
return None
|
|
604
649
|
|
|
605
650
|
|
|
606
651
|
# ----------------------
|
|
@@ -936,6 +936,10 @@ def _is_platform_compatible(metadata: dict, force: bool = False) -> bool:
|
|
|
936
936
|
platforms = metadata.get("platforms", [])
|
|
937
937
|
board_ver, _ = get_sima_build_version()
|
|
938
938
|
|
|
939
|
+
if not platforms:
|
|
940
|
+
click.echo("ℹ️ No platform restrictions specified; treating package as compatible with all platforms.")
|
|
941
|
+
return True
|
|
942
|
+
|
|
939
943
|
# Detect current OS and version
|
|
940
944
|
os_name = platform.system().lower() # 'darwin', 'windows', 'linux'
|
|
941
945
|
os_version = "Unknown"
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
METADATA_FILENAME = "metadata.json"
|
|
14
|
+
DEFAULT_POST_MESSAGE = "[bold]Package installed successfully.[/bold]\n"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _run_git(args: List[str], cwd: Path) -> Optional[str]:
|
|
18
|
+
try:
|
|
19
|
+
result = subprocess.run(
|
|
20
|
+
["git"] + args,
|
|
21
|
+
cwd=str(cwd),
|
|
22
|
+
check=True,
|
|
23
|
+
stdout=subprocess.PIPE,
|
|
24
|
+
stderr=subprocess.DEVNULL,
|
|
25
|
+
text=True,
|
|
26
|
+
)
|
|
27
|
+
except (OSError, subprocess.CalledProcessError):
|
|
28
|
+
return None
|
|
29
|
+
return result.stdout.strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _git_root(search_dir: Path) -> Optional[Path]:
|
|
33
|
+
output = _run_git(["rev-parse", "--show-toplevel"], search_dir)
|
|
34
|
+
if not output:
|
|
35
|
+
return None
|
|
36
|
+
return Path(output).resolve()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _github_repo_from_remote(remote_url: str) -> Optional[Tuple[str, str]]:
|
|
40
|
+
value = remote_url.strip()
|
|
41
|
+
if value.endswith(".git"):
|
|
42
|
+
value = value[:-4]
|
|
43
|
+
|
|
44
|
+
if value.startswith("git@github.com:"):
|
|
45
|
+
value = value[len("git@github.com:"):]
|
|
46
|
+
elif value.startswith("https://github.com/"):
|
|
47
|
+
value = value[len("https://github.com/"):]
|
|
48
|
+
elif value.startswith("http://github.com/"):
|
|
49
|
+
value = value[len("http://github.com/"):]
|
|
50
|
+
else:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
parts = value.split("/")
|
|
54
|
+
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
55
|
+
return None
|
|
56
|
+
return parts[0], parts[1]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_git_context(artifacts_folder: Path) -> Tuple[Optional[Path], Optional[Tuple[str, str]]]:
|
|
60
|
+
for search_dir in (Path.cwd(), artifacts_folder):
|
|
61
|
+
root = _git_root(search_dir)
|
|
62
|
+
if not root:
|
|
63
|
+
continue
|
|
64
|
+
remote = _run_git(["remote", "get-url", "origin"], root) or ""
|
|
65
|
+
return root, _github_repo_from_remote(remote)
|
|
66
|
+
return None, None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def default_package_name(artifacts_folder: Path) -> str:
|
|
70
|
+
git_root, github_repo = resolve_git_context(artifacts_folder)
|
|
71
|
+
if github_repo:
|
|
72
|
+
owner, repo = github_repo
|
|
73
|
+
return "gh:{}/{}".format(owner, repo)
|
|
74
|
+
if git_root:
|
|
75
|
+
return git_root.name
|
|
76
|
+
return Path.cwd().name
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def default_version(artifacts_folder: Path) -> str:
|
|
80
|
+
git_root, _github_repo = resolve_git_context(artifacts_folder)
|
|
81
|
+
if git_root:
|
|
82
|
+
tag = _run_git(["describe", "--tags", "--exact-match"], git_root)
|
|
83
|
+
if tag:
|
|
84
|
+
return tag
|
|
85
|
+
commit = _run_git(["rev-parse", "--short", "HEAD"], git_root)
|
|
86
|
+
if commit:
|
|
87
|
+
return commit
|
|
88
|
+
return datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def github_repo_description(artifacts_folder: Path) -> str:
|
|
92
|
+
_git_root, github_repo = resolve_git_context(artifacts_folder)
|
|
93
|
+
if not github_repo:
|
|
94
|
+
return ""
|
|
95
|
+
|
|
96
|
+
owner, repo = github_repo
|
|
97
|
+
headers = {"Accept": "application/vnd.github+json"}
|
|
98
|
+
token = os.getenv("GITHUB_TOKEN")
|
|
99
|
+
if token:
|
|
100
|
+
headers["Authorization"] = "Bearer {}".format(token)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
response = requests.get(
|
|
104
|
+
"https://api.github.com/repos/{}/{}".format(owner, repo),
|
|
105
|
+
headers=headers,
|
|
106
|
+
timeout=5,
|
|
107
|
+
)
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
description = response.json().get("description")
|
|
110
|
+
except Exception:
|
|
111
|
+
return ""
|
|
112
|
+
return description if isinstance(description, str) else ""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _sha256_file(path: Path) -> str:
|
|
116
|
+
hasher = hashlib.sha256()
|
|
117
|
+
with path.open("rb") as file_obj:
|
|
118
|
+
for chunk in iter(lambda: file_obj.read(1024 * 1024), b""):
|
|
119
|
+
hasher.update(chunk)
|
|
120
|
+
return hasher.hexdigest()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _format_size(total_bytes: int) -> str:
|
|
124
|
+
units = (("GB", 1024 ** 3), ("MB", 1024 ** 2), ("KB", 1024))
|
|
125
|
+
if total_bytes <= 0:
|
|
126
|
+
return "0KB"
|
|
127
|
+
for suffix, factor in units:
|
|
128
|
+
if total_bytes >= factor:
|
|
129
|
+
value = total_bytes / factor
|
|
130
|
+
text = "{:.1f}".format(value).rstrip("0").rstrip(".")
|
|
131
|
+
return "{}{}".format(text, suffix)
|
|
132
|
+
return "1KB"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _matches_exclude_pattern(resource: str, patterns: Sequence[str]) -> bool:
|
|
136
|
+
filename = Path(resource).name
|
|
137
|
+
return any(pattern and (pattern in resource or pattern in filename) for pattern in patterns)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def collect_artifact_resources(
|
|
141
|
+
artifacts_folder: Path,
|
|
142
|
+
exclude_patterns: Optional[Sequence[str]] = None,
|
|
143
|
+
) -> Tuple[List[str], Dict[str, Path], int]:
|
|
144
|
+
resources = []
|
|
145
|
+
resource_paths = {}
|
|
146
|
+
total_size = 0
|
|
147
|
+
normalized_excludes = [pattern.strip() for pattern in (exclude_patterns or []) if pattern.strip()]
|
|
148
|
+
|
|
149
|
+
for path in sorted(artifacts_folder.rglob("*")):
|
|
150
|
+
if not path.is_file():
|
|
151
|
+
continue
|
|
152
|
+
rel = path.relative_to(artifacts_folder).as_posix()
|
|
153
|
+
if rel == METADATA_FILENAME or re.fullmatch(r"metadata-[A-Za-z0-9_.-]+\.json", rel):
|
|
154
|
+
continue
|
|
155
|
+
if _matches_exclude_pattern(rel, normalized_excludes):
|
|
156
|
+
continue
|
|
157
|
+
resources.append(rel)
|
|
158
|
+
resource_paths[rel] = path
|
|
159
|
+
total_size += path.stat().st_size
|
|
160
|
+
|
|
161
|
+
return resources, resource_paths, total_size
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def parse_selectables(selectables: Optional[str]) -> List[Tuple[str, str]]:
|
|
165
|
+
if not selectables:
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
parsed = []
|
|
169
|
+
for raw_item in selectables.split(";"):
|
|
170
|
+
item = raw_item.strip()
|
|
171
|
+
if not item:
|
|
172
|
+
continue
|
|
173
|
+
if ":" not in item:
|
|
174
|
+
raise ValueError("selectables entries must use 'name:file' format")
|
|
175
|
+
name, resource = item.split(":", 1)
|
|
176
|
+
name = name.strip()
|
|
177
|
+
resource = resource.strip()
|
|
178
|
+
if not name or not resource:
|
|
179
|
+
raise ValueError("selectables entries must include both name and file")
|
|
180
|
+
parsed.append((name, resource))
|
|
181
|
+
return parsed
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def build_selectable_resources(
|
|
185
|
+
selectable_specs: List[Tuple[str, str]],
|
|
186
|
+
resource_paths: Dict[str, Path],
|
|
187
|
+
) -> List[Dict[str, str]]:
|
|
188
|
+
selectable_resources = []
|
|
189
|
+
seen_resources = set()
|
|
190
|
+
|
|
191
|
+
for name, resource in selectable_specs:
|
|
192
|
+
if resource not in resource_paths:
|
|
193
|
+
raise ValueError("selectable resource is not in artifacts-folder: {}".format(resource))
|
|
194
|
+
if resource in seen_resources:
|
|
195
|
+
raise ValueError("duplicate selectable resource: {}".format(resource))
|
|
196
|
+
seen_resources.add(resource)
|
|
197
|
+
selectable_resources.append({
|
|
198
|
+
"name": name,
|
|
199
|
+
"url": "",
|
|
200
|
+
"resource": resource,
|
|
201
|
+
"checksum": _sha256_file(resource_paths[resource]),
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
return selectable_resources
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def resolve_install_script(artifacts_folder: Path, install_script: str) -> str:
|
|
208
|
+
script = install_script.strip()
|
|
209
|
+
if not script:
|
|
210
|
+
raise ValueError("install-script cannot be empty")
|
|
211
|
+
|
|
212
|
+
candidate_paths = []
|
|
213
|
+
raw_path = Path(script).expanduser()
|
|
214
|
+
if raw_path.is_absolute():
|
|
215
|
+
candidate_paths.append(raw_path)
|
|
216
|
+
else:
|
|
217
|
+
candidate_paths.append(artifacts_folder / raw_path)
|
|
218
|
+
candidate_paths.append(Path.cwd() / raw_path)
|
|
219
|
+
|
|
220
|
+
artifacts_root = artifacts_folder.resolve()
|
|
221
|
+
for candidate in candidate_paths:
|
|
222
|
+
try:
|
|
223
|
+
resolved = candidate.resolve()
|
|
224
|
+
except OSError:
|
|
225
|
+
continue
|
|
226
|
+
if not resolved.is_file():
|
|
227
|
+
continue
|
|
228
|
+
try:
|
|
229
|
+
rel = resolved.relative_to(artifacts_root).as_posix()
|
|
230
|
+
except ValueError:
|
|
231
|
+
continue
|
|
232
|
+
return "./{}".format(rel)
|
|
233
|
+
|
|
234
|
+
return script
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def build_metadata(
|
|
238
|
+
artifacts_folder: Path,
|
|
239
|
+
name: Optional[str] = None,
|
|
240
|
+
version: Optional[str] = None,
|
|
241
|
+
description: Optional[str] = None,
|
|
242
|
+
install_script: str = "",
|
|
243
|
+
selectables: Optional[str] = None,
|
|
244
|
+
exclude: Optional[Sequence[str]] = None,
|
|
245
|
+
) -> Dict:
|
|
246
|
+
artifacts_folder = artifacts_folder.expanduser().resolve()
|
|
247
|
+
if not artifacts_folder.is_dir():
|
|
248
|
+
raise ValueError("artifacts-folder is not a directory: {}".format(artifacts_folder))
|
|
249
|
+
|
|
250
|
+
resources, resource_paths, total_size = collect_artifact_resources(artifacts_folder, exclude_patterns=exclude)
|
|
251
|
+
if not resources:
|
|
252
|
+
raise ValueError("artifacts-folder does not contain any artifact files")
|
|
253
|
+
|
|
254
|
+
selectable_specs = parse_selectables(selectables)
|
|
255
|
+
selectable_resources = build_selectable_resources(selectable_specs, resource_paths)
|
|
256
|
+
selectable_resource_names = {entry["resource"] for entry in selectable_resources}
|
|
257
|
+
resources = [resource for resource in resources if resource not in selectable_resource_names]
|
|
258
|
+
checksums = {
|
|
259
|
+
resource: _sha256_file(path)
|
|
260
|
+
for resource, path in resource_paths.items()
|
|
261
|
+
if resource not in selectable_resource_names
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
resolved_description = description
|
|
265
|
+
if resolved_description is None:
|
|
266
|
+
resolved_description = github_repo_description(artifacts_folder)
|
|
267
|
+
|
|
268
|
+
size_text = _format_size(total_size)
|
|
269
|
+
return {
|
|
270
|
+
"name": name or default_package_name(artifacts_folder),
|
|
271
|
+
"version": version or default_version(artifacts_folder),
|
|
272
|
+
"release": "",
|
|
273
|
+
"description": resolved_description or "",
|
|
274
|
+
"platforms": [],
|
|
275
|
+
"resources": resources,
|
|
276
|
+
"resources-checksum": checksums,
|
|
277
|
+
"selectable-resources": selectable_resources,
|
|
278
|
+
"size": {
|
|
279
|
+
"download": size_text,
|
|
280
|
+
"install": size_text,
|
|
281
|
+
},
|
|
282
|
+
"installation": {
|
|
283
|
+
"script": resolve_install_script(artifacts_folder, install_script),
|
|
284
|
+
"post-message": DEFAULT_POST_MESSAGE,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def metadata_filename(variant: Optional[str] = None) -> str:
|
|
290
|
+
if variant is None:
|
|
291
|
+
return METADATA_FILENAME
|
|
292
|
+
|
|
293
|
+
normalized = variant.strip()
|
|
294
|
+
if not normalized:
|
|
295
|
+
return METADATA_FILENAME
|
|
296
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", normalized):
|
|
297
|
+
raise ValueError("variant must contain only letters, numbers, dots, underscores, or hyphens")
|
|
298
|
+
return "metadata-{}.json".format(normalized)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def write_metadata(artifacts_folder: Path, metadata: Dict, variant: Optional[str] = None) -> Path:
|
|
302
|
+
output_path = artifacts_folder.expanduser().resolve() / metadata_filename(variant)
|
|
303
|
+
output_path.write_text(json.dumps(metadata, indent=4) + "\n", encoding="utf-8")
|
|
304
|
+
return output_path
|
|
@@ -19,6 +19,7 @@ import json
|
|
|
19
19
|
import click
|
|
20
20
|
import datetime
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
from sima_cli.install.package_builder import build_metadata, write_metadata
|
|
22
23
|
from rich.table import Table
|
|
23
24
|
from rich.console import Console
|
|
24
25
|
from rich.syntax import Syntax
|
|
@@ -74,6 +75,54 @@ def show_metadata(name, version):
|
|
|
74
75
|
except ValueError as e:
|
|
75
76
|
Console().print(f"[red]Error:[/red] {e}")
|
|
76
77
|
|
|
78
|
+
|
|
79
|
+
@packages.command("build", help="Build package metadata.json from an artifacts folder.")
|
|
80
|
+
@click.argument(
|
|
81
|
+
"artifacts_folder",
|
|
82
|
+
metavar="ARTIFACTS_FOLDER",
|
|
83
|
+
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
|
|
84
|
+
)
|
|
85
|
+
@click.option("--name", help="Package name. Defaults to gh:<org>/<repo> for GitHub repos.")
|
|
86
|
+
@click.option("--version", help="Package version. Defaults to exact git tag or short commit hash.")
|
|
87
|
+
@click.option("--description", help="Package description. Defaults to the GitHub repo description when available.")
|
|
88
|
+
@click.option(
|
|
89
|
+
"--install-script",
|
|
90
|
+
required=True,
|
|
91
|
+
help="Install script file inside ARTIFACTS_FOLDER, or a single-line shell command.",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--selectables",
|
|
95
|
+
help="Optional resources in 'name1:file1;name2:file2' format.",
|
|
96
|
+
)
|
|
97
|
+
@click.option(
|
|
98
|
+
"--exclude",
|
|
99
|
+
multiple=True,
|
|
100
|
+
help="Exclude artifact files whose relative path or filename contains this text. May be repeated.",
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--variant",
|
|
104
|
+
help="Optional metadata variant name. Writes metadata-<variant>.json instead of metadata.json.",
|
|
105
|
+
)
|
|
106
|
+
def build_package_metadata(artifacts_folder, name, version, description, install_script, selectables, exclude, variant):
|
|
107
|
+
"""
|
|
108
|
+
Generate metadata.json, or metadata-<variant>.json, for sima-cli package installation.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
metadata = build_metadata(
|
|
112
|
+
artifacts_folder=artifacts_folder,
|
|
113
|
+
name=name,
|
|
114
|
+
version=version,
|
|
115
|
+
description=description,
|
|
116
|
+
install_script=install_script,
|
|
117
|
+
selectables=selectables,
|
|
118
|
+
exclude=exclude,
|
|
119
|
+
)
|
|
120
|
+
output_path = write_metadata(artifacts_folder, metadata, variant=variant)
|
|
121
|
+
except ValueError as exc:
|
|
122
|
+
raise click.ClickException(str(exc))
|
|
123
|
+
|
|
124
|
+
click.echo(f"✅ Package metadata written to: {output_path}")
|
|
125
|
+
|
|
77
126
|
# ------------------------------------------------------------------------------------------------------------
|
|
78
127
|
# Register the group to main CLI entrypoint, skip this group if it's running inside the SDK container already
|
|
79
128
|
# ------------------------------------------------------------------------------------------------------------
|
|
@@ -38,7 +38,7 @@ console = Console()
|
|
|
38
38
|
# ------------------------------------------------------------
|
|
39
39
|
# Group Definition
|
|
40
40
|
# ------------------------------------------------------------
|
|
41
|
-
@click.group(
|
|
41
|
+
@click.group(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
42
42
|
@click.option(
|
|
43
43
|
"-v", "--version",
|
|
44
44
|
"version_filter",
|