Skip to content

Commit 1984ca4

Browse files
committed
cli: Add deploy command
This allows you to deploy a configuration to a directory: cloe-launch -v deploy -P engine/tests/conanfile_with_server.py /usr/local This deployment should work without any further setup required. An uninstaller is created at ~/.cache/cloe/launcher/UUID/uinstall.sh where UUID is unique and determined from the configuration.
1 parent 9f921ec commit 1984ca4

File tree

3 files changed

+257
-24
lines changed

3 files changed

+257
-24
lines changed

cli/cloe_launch/__main__.py

+53-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
import logging
2121
import os
2222
import sys
23+
import pathlib
2324

24-
from typing import Dict
2525
from typing import List
2626

2727
import click
@@ -351,6 +351,58 @@ def cli_prepare(
351351
sys.exit(1)
352352

353353

354+
# _________________________________________________________________________
355+
# Command: deploy [--cache] [--profile=PROFILE | --profile-path=CONANFILE]
356+
@main.command("deploy")
357+
@options.profile()
358+
@options.profile_path()
359+
@options.conan_arg()
360+
@options.conan_option()
361+
@options.conan_setting()
362+
@click.option(
363+
"--rpath/--no-rpath",
364+
is_flag=True,
365+
default=True,
366+
help="Set the RPATH of all binaries and libraries.",
367+
)
368+
@click.option("--force", is_flag=True, help="Overwrite existing files.")
369+
@click.argument("path")
370+
@click.pass_obj
371+
def cli_deploy(
372+
opt,
373+
profile: str,
374+
profile_path: str,
375+
conan_arg: List[str],
376+
conan_option: List[str],
377+
conan_setting: List[str],
378+
force: bool,
379+
rpath: bool,
380+
path: str,
381+
) -> None:
382+
"""Deploy environment for selected profile.
383+
384+
This may involve downloading missing and available packages and building
385+
outdated packages.
386+
"""
387+
options.deny_profile_and_path(profile, profile_path)
388+
conf = Configuration(profile)
389+
engine = Engine(conf, conanfile=profile_path)
390+
engine.conan_args = list(conan_arg)
391+
engine.conan_options = list(conan_option)
392+
engine.conan_settings = list(conan_setting)
393+
394+
try:
395+
engine.deploy(
396+
pathlib.Path(path),
397+
patch_rpath=rpath,
398+
)
399+
except ChildProcessError:
400+
# Most likely scenario:
401+
# 1. conan had an error and terminated with non-zero error
402+
# 2. error has already been logged
403+
sys.exit(1)
404+
405+
354406
# _________________________________________________________________________
355407
# Command: clean [--profile PROFILE | --profile-path=CONANFILE]
356408
@main.command("clean")

cli/cloe_launch/exec.py

+147-20
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,28 @@
1111
import importlib.util
1212
import logging
1313
import os
14+
import json
1415
import re
1516
import shutil
1617
import subprocess
1718
import sys
19+
import platform
1820
import textwrap
1921
import shlex
2022

2123
from pathlib import Path
2224
from collections import OrderedDict
2325
from typing import Dict
26+
from typing import Set
2427
from typing import List
2528
from typing import Mapping
2629
from typing import Optional
2730
from typing import Type
2831
from typing import Union
2932

30-
from cloe_launch.utility import run_cmd
3133
from cloe_launch import Configuration
34+
import cloe_launch.utility as cloe_utils
35+
from cloe_launch.utility import run_cmd
3236

3337

3438
class Environment:
@@ -403,17 +407,18 @@ def runtime_env_path(self) -> Path:
403407
"""Return the path to the list of pruned environment variables."""
404408
return self.runtime_dir / "environment_all.sh.env"
405409

406-
def _prepare_runtime_dir(self) -> None:
410+
def _prepare_runtime_dir(self, with_json: bool = False) -> None:
407411
"""Clean and create runtime directory."""
408412
self.clean()
409413
logging.debug(f"Create: {self.runtime_dir}")
410414
self.runtime_dir.mkdir(parents=True)
411-
self._prepare_virtualenv()
415+
self._prepare_virtualenv(with_json)
412416
self._write_cloe_env()
413417
self._write_activate_all(
414418
[
415419
# From Conan VirtualRunEnv (!= virtualrunenv) generator:
416-
self.runtime_dir / "conanrun.sh",
420+
self.runtime_dir
421+
/ "conanrun.sh",
417422
],
418423
[
419424
# From Conan virtualenv generator:
@@ -424,7 +429,7 @@ def _prepare_runtime_dir(self) -> None:
424429
self.runtime_dir / "environment_cloe_launch.sh.env",
425430
# From self._write_cloe_env(), derived from environment_run.sh:
426431
self.runtime_dir / "environment_cloe.sh.env",
427-
]
432+
],
428433
)
429434
self._write_prompt_sh()
430435
self._write_bashrc()
@@ -496,21 +501,35 @@ def _write_zshrc(self) -> None:
496501

497502
def _write_cloe_env(self) -> None:
498503
"""Derive important CLOE_ variables and write environment_cloe.sh.env file."""
499-
conanrun = self.runtime_dir / "conanrun.sh" # From newer VirtualRunEnv generator
500-
activate_run = self.runtime_dir / "activate_run.sh" # From older virtualrunenv generator
504+
conanrun = (
505+
self.runtime_dir / "conanrun.sh"
506+
) # From newer VirtualRunEnv generator
507+
activate_run = (
508+
self.runtime_dir / "activate_run.sh"
509+
) # From older virtualrunenv generator
501510
if conanrun.exists():
502511
if activate_run.exists():
503-
logging.warning("Warning: Found both conanrun.sh and activate_run.sh in runtime directory!")
512+
logging.warning(
513+
"Warning: Found both conanrun.sh and activate_run.sh in runtime directory!"
514+
)
504515
logging.warning("Note:")
505-
logging.warning(" It looks like /both/ VirtualRunEnv and virtualrunenv generators are being run.")
506-
logging.warning(" This may come from using an out-of-date cloe-launch-profile package.")
516+
logging.warning(
517+
" It looks like /both/ VirtualRunEnv and virtualrunenv generators are being run."
518+
)
519+
logging.warning(
520+
" This may come from using an out-of-date cloe-launch-profile package."
521+
)
507522
logging.warning("")
508-
logging.warning(" Continuing with hybrid approach. Environment variables may be incorrectly set.")
509-
env = Environment(conanrun, source_file = True)
523+
logging.warning(
524+
" Continuing with hybrid approach. Environment variables may be incorrectly set."
525+
)
526+
env = Environment(conanrun, source_file=True)
510527
elif activate_run.exists():
511-
env = Environment(activate_run, source_file = True)
528+
env = Environment(activate_run, source_file=True)
512529
else:
513-
raise RuntimeError("cannot find conanrun.sh or activate_run.sh in runtime directory")
530+
raise RuntimeError(
531+
"cannot find conanrun.sh or activate_run.sh in runtime directory"
532+
)
514533

515534
if env.has("CLOE_SHELL"):
516535
logging.error("Error: recursive cloe shells are not supported.")
@@ -531,7 +550,9 @@ def _write_cloe_env(self) -> None:
531550
cloe_env.path_set("CLOE_PLUGIN_PATH", self._extract_plugin_paths(env))
532551
cloe_env.export(self.runtime_dir / "environment_cloe.sh.env")
533552

534-
def _write_activate_all(self, source_files: List[Path], env_files: List[Path]) -> None:
553+
def _write_activate_all(
554+
self, source_files: List[Path], env_files: List[Path]
555+
) -> None:
535556
"""Write activate_all.sh file."""
536557
activate_file = self.runtime_dir / "activate_all.sh"
537558
activate_data = textwrap.dedent(
@@ -566,7 +587,7 @@ def _write_activate_all(self, source_files: List[Path], env_files: List[Path]) -
566587
with activate_file.open("w") as file:
567588
file.write(activate_data)
568589

569-
def _prepare_virtualenv(self) -> None:
590+
def _prepare_virtualenv(self, with_json: bool = False) -> None:
570591
# Get conan to create a virtualenv AND virtualrunenv for us:
571592
# One gives us the LD_LIBRARY_PATH and the other gives us env_info
572593
# variables set in packages.
@@ -578,6 +599,9 @@ def _prepare_virtualenv(self) -> None:
578599
"-g",
579600
"VirtualRunEnv",
580601
]
602+
if with_json:
603+
conan_cmd.append("-g")
604+
conan_cmd.append("json")
581605
for arg in self.conan_args:
582606
conan_cmd.append(arg)
583607
for option in self.conan_options:
@@ -600,8 +624,12 @@ def _extract_engine_path(self, env: Environment) -> Path:
600624
logging.error("Note:")
601625
logging.error(" This problem usually stems from one of two common errors:")
602626
logging.error(" - The conanfile for cloe-launch does not require cloe-engine.")
603-
logging.error(" - The cloe-engine package or binary has not been built / is corrupted.")
604-
logging.error(" However, unconvential or unsupported package configuration may also trigger this.")
627+
logging.error(
628+
" - The cloe-engine package or binary has not been built / is corrupted."
629+
)
630+
logging.error(
631+
" However, unconvential or unsupported package configuration may also trigger this."
632+
)
605633
sys.exit(2)
606634

607635
def _extract_plugin_paths(self, env: Environment) -> List[Path]:
@@ -623,12 +651,14 @@ def _extract_plugin_setups(self, lib_paths: List[Path]) -> List[Type[PluginSetup
623651
_find_plugin_setups(path)
624652
return PluginSetup.__subclasses__()
625653

626-
def _prepare_runtime_env(self, use_cache: bool = False) -> Environment:
654+
def _prepare_runtime_env(
655+
self, use_cache: bool = False, with_json: bool = False
656+
) -> Environment:
627657
if self.runtime_env_path().exists() and use_cache:
628658
logging.debug("Re-using existing runtime directory.")
629659
else:
630660
logging.debug("Initializing runtime directory ...")
631-
self._prepare_runtime_dir()
661+
self._prepare_runtime_dir(with_json=with_json)
632662

633663
# Get environment variables we need:
634664
return Environment(
@@ -758,6 +788,103 @@ def prepare(self, build_policy: str = "outdated") -> None:
758788
self.conan_args.append(f"--build={build_policy}")
759789
self._prepare_runtime_env(use_cache=False)
760790

791+
def deploy(
792+
self,
793+
dest: Path,
794+
wrapper: Optional[Path] = None,
795+
wrapper_target: Optional[Path] = None,
796+
patch_rpath: bool = True,
797+
build_policy: str = "outdated",
798+
) -> None:
799+
"""Deploy dependencies for the profile."""
800+
self.capture_output = False
801+
if build_policy == "":
802+
self.conan_args.append("--build")
803+
else:
804+
self.conan_args.append(f"--build={build_policy}")
805+
self._prepare_runtime_env(use_cache=False, with_json=True)
806+
807+
# Ensure destination exists:
808+
if not dest.is_dir():
809+
if dest.exists():
810+
logging.error(f"Error: destination is not a directory: {dest}")
811+
sys.exit(1)
812+
dest.mkdir(parents=True)
813+
814+
# Copy necessary files to destination:
815+
# TODO: Create a manifest and be verbose about files being copied.
816+
build_info = self.runtime_dir / "conanbuildinfo.json"
817+
logging.info(f"Reading: {build_info}")
818+
build_data = json.load(build_info.open())
819+
install_manifest: List[Path] = []
820+
821+
def copy_file(src, dest):
822+
install_manifest.append(Path(dest))
823+
logging.info(f"Installing: {dest}")
824+
return shutil.copy2(src, dest)
825+
826+
def copy_tree(src, dest, ignore):
827+
if src.find("/build/") != -1:
828+
logging.warning(
829+
f"Warning: deploying from build directory is strongly discouraged: {dep['rootpath']}"
830+
)
831+
shutil.copytree(
832+
src,
833+
dest,
834+
copy_function=copy_file,
835+
dirs_exist_ok=True,
836+
ignore=shutil.ignore_patterns(*ignore),
837+
)
838+
839+
for dep in build_data["dependencies"]:
840+
for src in dep["bin_paths"]:
841+
copy_tree(src, dest/"bin", ignore=["bzip2"])
842+
for src in dep["lib_paths"]:
843+
copy_tree(src, dest/"lib", ignore=["cmake", "*.a"])
844+
845+
# Patching RPATH of all the binaries lets everything run
846+
# fine without any extra steps, like setting LD_LIBRARY_PATH.
847+
if patch_rpath:
848+
assert platform.system() != "Windows"
849+
cloe_utils.patch_binary_files_rpath(dest / "bin", ["$ORIGIN/../lib"])
850+
cloe_utils.patch_binary_files_rpath(dest / "lib" / "cloe", ["$ORIGIN/.."])
851+
852+
if wrapper is not None:
853+
if wrapper_target is None:
854+
wrapper_target = dest / "bin" / "cloe-engine"
855+
wrapper_data = textwrap.dedent(
856+
f"""\
857+
#!/bin/sh
858+
859+
{wrapper_target} $@
860+
"""
861+
)
862+
with wrapper.open("w") as wrapper_file:
863+
wrapper_file.write(wrapper_data)
864+
865+
def simplify_manifest(manifest: Set[Path]):
866+
for path in list(manifest):
867+
parent = path.parent
868+
while parent != parent.parent:
869+
if parent in manifest:
870+
manifest.remove(parent)
871+
parent = parent.parent
872+
873+
# Create uninstaller from manifest
874+
uninstaller_file = self.runtime_dir / "uninstall.sh"
875+
logging.info(f"Write: {uninstaller_file}")
876+
with uninstaller_file.open("w") as f:
877+
install_dirs: Set[Path] = set()
878+
f.write("#!/bin/bash\n")
879+
for file in install_manifest:
880+
install_dirs.add(file.parent)
881+
f.write(f"echo 'Removing file: {file}'\n")
882+
f.write(f"rm '{file}'\n")
883+
simplify_manifest(install_dirs)
884+
for path in install_dirs:
885+
f.write(f"echo 'Removing dir: {path}'\n")
886+
f.write(f"rmdir -p '{path}'\n")
887+
761888
def exec(
762889
self,
763890
args: List[str],

0 commit comments

Comments
 (0)