def _pack_charm(self, parsed_args) -> List[pathlib.Path]: """Pack a charm.""" emit.progress("Packing the charm.") # adapt arguments to use the build infrastructure build_args = Namespace( **{ "debug": parsed_args.debug, "destructive_mode": parsed_args.destructive_mode, "from": self.config.project.dirpath, "entrypoint": parsed_args.entrypoint, "requirement": parsed_args.requirement, "shell": parsed_args.shell, "shell_after": parsed_args.shell_after, "bases_indices": parsed_args.bases_index, "force": parsed_args.force, }) # mimic the "build" command validator = build.Validator(self.config) args = validator.process(build_args) emit.trace(f"Working arguments: {args}") builder = build.Builder(args, self.config) charms = builder.run(parsed_args.bases_index, destructive_mode=build_args.destructive_mode) emit.message("Charms packed:") for charm in charms: emit.message(f" {charm}")
def get_parallel_build_count() -> int: """Obtain the number of concurrent jobs to execute. Try different strategies to obtain the number of parallel jobs to execute. If they fail, assume the safe default of 1. The number of concurrent build jobs can be limited by setting the environment variable ``SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT``. :return: The number of parallel jobs for the current host. """ try: build_count = len(os.sched_getaffinity(0)) emit.debug(f"CPU count (from process affinity): {build_count}") except AttributeError: # Fall back to multiprocessing.cpu_count()... try: build_count = multiprocessing.cpu_count() emit.debug(f"CPU count (from multiprocessing): {build_count}") except NotImplementedError: emit.progress( "Unable to determine CPU count; disabling parallel builds", permanent=True, ) build_count = 1 try: max_count = int( os.environ.get("SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT", "")) if max_count > 0: build_count = min(build_count, max_count) except ValueError: emit.debug("Invalid SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT value") return build_count
def _check_snap_dir(snap_dir_path: Path) -> None: """Verify if the given path only contains expected files.""" unexpected_paths = set() for root, directories, files in os.walk(snap_dir_path): for entry in directories + files: path = Path(root, entry) relpath = path.relative_to(snap_dir_path) if not _snap_dir_path_expected(relpath): unexpected_paths.add(str(relpath)) if unexpected_paths: snap_dir_relpath = snap_dir_path.relative_to(Path()) emit.progress( "The {snap_dir!r} directory is meant specifically for snapcraft, but it contains\n" "the following non-snapcraft-related paths:" "\n- {unexpected_files}\n\n" "This is unsupported and may cause unexpected behavior. If you must store\n" "these files within the {snap_dir!r} directory, move them to {snap_dir_local!r}\n" "which is ignored by snapcraft.".format( snap_dir=str(snap_dir_relpath), snap_dir_local=str(snap_dir_relpath / "local"), unexpected_files="\n- ".join(sorted(unexpected_paths)), ), permanent=True, )
def extract_metadata(self) -> List[ExtractedMetadata]: """Obtain metadata information.""" if self._adopt_info is None or self._adopt_info not in self._parse_info: return [] dirs = ProjectDirs(work_dir=self._work_dir) part = Part(self._adopt_info, {}, project_dirs=dirs) locations = ( part.part_src_dir, part.part_build_dir, part.part_install_dir, ) metadata_list: List[ExtractedMetadata] = [] for metadata_file in self._parse_info[self._adopt_info]: emit.trace(f"extract metadata: parse info from {metadata_file}") for location in locations: if pathlib.Path(location, metadata_file.lstrip("/")).is_file(): metadata = extract_metadata(metadata_file, workdir=str(location)) if metadata: metadata_list.append(metadata) break emit.progress( f"No metadata extracted from {metadata_file}", permanent=True) return metadata_list
def _extract_file(self, image_tar: str, name: str, compress: bool = False) -> (str, int, str): """Extract a file from the tar and return its info. Optionally, gzip the content.""" emit.progress(f"Extracting file {name!r} from local tar (compress={compress})") src_filehandler = image_tar.extractfile(name) mtime = image_tar.getmember(name).mtime hashing_temp_file = HashingTemporaryFile() if compress: # open the gzip file using the temporary file handler; use the original name and time # as 'filename' and 'mtime' correspondingly as those go to the gzip headers, # to ensure same final hash across different runs dst_filehandler = gzip.GzipFile( fileobj=hashing_temp_file, mode="wb", filename=os.path.basename(name), mtime=mtime, ) else: dst_filehandler = hashing_temp_file try: while True: chunk = src_filehandler.read(CHUNK_SIZE) if not chunk: break dst_filehandler.write(chunk) finally: dst_filehandler.close() # gzip does not automatically close the underlying file handler, let's do it manually hashing_temp_file.close() digest = "sha256:{}".format(hashing_temp_file.hexdigest) return hashing_temp_file.name, hashing_temp_file.total_length, digest
def _generate_manifest( project: Project, *, lifecycle: PartsLifecycle, start_time: datetime, parsed_args: "argparse.Namespace", ) -> None: """Create and populate the manifest file.""" emit.progress("Generating snap manifest...") image_information = parsed_args.manifest_image_information or "{}" parts = copy.deepcopy(project.parts) for name, part in parts.items(): assets = lifecycle.get_part_pull_assets(part_name=name) if assets: part["stage-packages"] = assets.get("stage-packages", []) or [] for key in ("stage", "prime", "stage-packages", "build-packages"): part.setdefault(key, []) manifest.write( project, lifecycle.prime_dir, arch=lifecycle.target_arch, parts=parts, start_time=start_time, image_information=image_information, primed_stage_packages=lifecycle.get_primed_stage_packages(), ) emit.progress("Generated snap manifest", permanent=True) # Also copy the original snapcraft.yaml snap_project = get_snap_project() shutil.copy(snap_project.project_file, lifecycle.prime_dir / "snap")
def error_decorator(self, *args, **kwargs): """Handle craft-store error situations and login scenarios.""" try: return method(self, *args, **kwargs) except craft_store.errors.CredentialsUnavailable: if os.getenv(ALTERNATE_AUTH_ENV_VAR): raise RuntimeError( "Charmcraft error: internal inconsistency detected " "(CredentialsUnavailable error while having user provided credentials)." ) if not auto_login: raise emit.progress("Credentials not found. Trying to log in...") except craft_store.errors.StoreServerError as error: if error.response.status_code == 401: if os.getenv(ALTERNATE_AUTH_ENV_VAR): raise CraftError( "Provided credentials are no longer valid for Charmhub. " "Regenerate them and try again." ) if not auto_login: raise CraftError("Existing credentials are no longer valid for Charmhub.") emit.progress("Existing credentials no longer valid. Trying to log in...") # Clear credentials before trying to login again self.logout() else: raise CraftError(str(error)) from error self.login() return method(self, *args, **kwargs)
def _pack_charm(self, parsed_args) -> List[pathlib.Path]: """Pack a charm.""" emit.progress("Packing the charm.") # adapt arguments to use the build infrastructure build_args = Namespace( **{ "debug": parsed_args.debug, "destructive_mode": parsed_args.destructive_mode, "from": self.config.project.dirpath, "entrypoint": parsed_args.entrypoint, "requirement": parsed_args.requirement, "shell": parsed_args.shell, "shell_after": parsed_args.shell_after, "bases_indices": parsed_args.bases_index, "force": parsed_args.force, }) # mimic the "build" command validator = build.Validator(self.config) args = validator.process(build_args) emit.trace(f"Working arguments: {args}") builder = build.Builder(args, self.config) charms = builder.run(parsed_args.bases_index, destructive_mode=build_args.destructive_mode) # avoid showing results when run inside a container (the outer charmcraft # is responsible of the final message to the user) if not env.is_charmcraft_running_in_managed_mode(): emit.message("Charms packed:") for charm in charms: emit.message(f" {charm}")
def _upload(self, endpoint, filepath, *, extra_fields=None): """Upload for all charms, bundles and resources (generic process).""" upload_id = self._client.push_file(filepath) payload = {"upload-id": upload_id} if extra_fields is not None: payload.update(extra_fields) response = self._client.request_urlpath_json("POST", endpoint, json=payload) status_url = response["status-url"] emit.progress(f"Upload {upload_id} started, got status url {status_url}") while True: response = self._client.request_urlpath_json("GET", status_url) emit.progress(f"Status checked: {response}") # as we're asking for a single upload_id, the response will always have only one item (revision,) = response["revisions"] status = revision["status"] if status in UPLOAD_ENDING_STATUSES: return Uploaded( ok=UPLOAD_ENDING_STATUSES[status], errors=_build_errors(revision), status=status, revision=revision["revision"], ) # XXX Facundo 2020-06-30: Implement a slight backoff algorithm and fallout after # N attempts (which should be big, as per snapcraft experience). Issue: #79. time.sleep(POLL_DELAY)
def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse.Namespace") -> None: """Pack image in provider instance.""" emit.debug("Checking build provider availability") provider_name = "lxd" if parsed_args.use_lxd else None provider = providers.get_provider(provider_name) provider.ensure_provider_is_available() cmd = ["snapcraft", command_name] if hasattr(parsed_args, "parts"): cmd.extend(parsed_args.parts) if getattr(parsed_args, "output", None): cmd.extend(["--output", parsed_args.output]) if emit.get_mode() == EmitterMode.VERBOSE: cmd.append("--verbose") elif emit.get_mode() == EmitterMode.QUIET: cmd.append("--quiet") elif emit.get_mode() == EmitterMode.DEBUG: cmd.append("--verbosity=debug") elif emit.get_mode() == EmitterMode.TRACE: cmd.append("--verbosity=trace") if parsed_args.debug: cmd.append("--debug") if getattr(parsed_args, "shell", False): cmd.append("--shell") if getattr(parsed_args, "shell_after", False): cmd.append("--shell-after") if getattr(parsed_args, "enable_manifest", False): cmd.append("--enable-manifest") build_information = getattr(parsed_args, "manifest_build_information", None) if build_information: cmd.append("--manifest-build-information") cmd.append(build_information) output_dir = utils.get_managed_environment_project_path() emit.progress("Launching instance...") with provider.launched_environment( project_name=project.name, project_path=Path().absolute(), base=project.get_effective_base(), bind_ssh=parsed_args.bind_ssh, build_on=get_host_architecture(), build_for=get_host_architecture(), ) as instance: try: with emit.pause(): instance.execute_run(cmd, check=True, cwd=output_dir) capture_logs_from_instance(instance) except subprocess.CalledProcessError as err: capture_logs_from_instance(instance) raise providers.ProviderError( f"Failed to execute {command_name} in instance.") from err
def _launch_shell(*, cwd: Optional[pathlib.Path] = None) -> None: """Launch a user shell for debugging environment. :param cwd: Working directory to start user in. """ emit.progress("Launching shell on build environment...", permanent=True) with emit.pause(): subprocess.run(["bash"], check=False, cwd=cwd)
def is_blob_already_uploaded(self, reference): """Verify if the blob is already uploaded, using a generic reference. If yes, return its digest. """ emit.progress("Checking if the blob is already uploaded") url = self._get_url("blobs/{}".format(reference)) return self._is_item_already_uploaded(url)
def run(self, parsed_args): """Run the command.""" project_path = self.config.project.dirpath metadata = parse_metadata_yaml(project_path) emit.progress(f"Cleaning project {metadata.name!r}.") provider = get_provider() provider.clean_project_environments(charm_name=metadata.name, project_path=project_path) emit.message(f"Cleaned project {metadata.name!r}.")
def pack_snap( directory: Path, *, output: Optional[str], compression: Optional[str] = None, name: Optional[str] = None, version: Optional[str] = None, target_arch: Optional[str] = None, ) -> str: """Pack snap contents with `snap pack`. `output` may either be a directory, a file path, or just a file name. - directory: write snap to directory with default snap name - file path: write snap to specified directory with specified snap name - file name: write snap to cwd with specified snap name If name, version, and target architecture are not specified, then snap will use its default naming convention. :param directory: Directory to pack. :param output: Snap file name or directory. :param compression: Compression type to use, None for defaults. :param name: Name of snap project. :param version: Version of snap project. :param target_arch: Target architecture the snap project is built to. """ emit.debug(f"pack_snap: output={output!r}, compression={compression!r}") # TODO remove workaround once LP: #1950465 is fixed _verify_snap(directory) # create command formatted as `snap pack <options> <snap-dir> <output-dir>` command: List[Union[str, Path]] = ["snap", "pack"] output_file = _get_filename(output, name, version, target_arch) if output_file is not None: command.extend(["--filename", output_file]) if compression is not None: command.extend(["--compression", compression]) command.append(directory) command.append(_get_directory(output)) emit.progress("Creating snap package...") emit.debug(f"Pack command: {command}") try: proc = subprocess.run(command, capture_output=True, check=True, universal_newlines=True) except subprocess.CalledProcessError as err: msg = f"Cannot pack snap file: {err!s}" if err.stderr: msg += f" ({err.stderr.strip()!s})" raise errors.SnapcraftError(msg) snap_filename = Path(str(proc.stdout).partition(":")[2].strip()).name return snap_filename
def _upload_blob(self, filepath: str, size: int, digest: str) -> None: """Upload the blob (if necessary).""" # if it's already uploaded, nothing to do if self.registry.is_blob_already_uploaded(digest): emit.progress("Blob was already uploaded") else: self.registry.upload_blob(filepath, size, digest) # finally remove the temp filepath os.unlink(filepath)
def upload_manifest(self, manifest_data, reference): """Upload a manifest.""" url = self._get_url("manifests/{}".format(reference)) headers = { "Content-Type": MANIFEST_V2_MIMETYPE, } emit.progress(f"Uploading manifest with reference {reference}") response = self._hit("PUT", url, headers=headers, data=manifest_data.encode("utf8")) assert_response_ok(response, expected_status=201) emit.progress("Manifest uploaded OK")
def build_charm(self) -> None: """Build the charm.""" emit.progress(f"Building charm in {str(self.buildpath)!r}") if self.buildpath.exists(): shutil.rmtree(str(self.buildpath)) self.buildpath.mkdir() linked_entrypoint = self.handle_generic_paths() self.handle_dispatcher(linked_entrypoint) self.handle_dependencies()
def _is_valid_elf(self, resolved_path: Path) -> bool: if not resolved_path.exists() or not ElfFile.is_elf(resolved_path): return False try: elf_file = ElfFile(path=resolved_path) except errors.CorruptedElfFile as error: # Warn if the ELF file seems corrupted. emit.progress(str(error), permanent=True) return False return elf_file.arch_tuple == self.arch_tuple
def build_charm(self, bases_config: BasesConfiguration) -> str: """Build the charm. :param bases_config: Bases configuration to use for build. :returns: File name of charm. :raises CraftError: on lifecycle exception. :raises RuntimeError: on unexpected lifecycle exception. """ if env.is_charmcraft_running_in_managed_mode(): work_dir = env.get_managed_environment_home_path() else: work_dir = self.buildpath emit.progress(f"Building charm in {str(work_dir)!r}") if self._special_charm_part: # all current deprecated arguments set charm plugin parameters self._handle_deprecated_cli_arguments() # add charm files to the prime filter self._set_prime_filter() # set source if empty or not declared in charm part if not self._special_charm_part.get("source"): self._special_charm_part["source"] = str(self.charmdir) # run the parts lifecycle emit.trace(f"Parts definition: {self._parts}") lifecycle = parts.PartsLifecycle( self._parts, work_dir=work_dir, project_dir=self.charmdir, project_name=self.metadata.name, ignore_local_sources=["*.charm"], ) lifecycle.run(Step.PRIME) # run linters and show the results linting_results = linters.analyze(self.config, lifecycle.prime_dir) self.show_linting_results(linting_results) create_manifest( lifecycle.prime_dir, self.config.project.started_at, bases_config, linting_results, ) zipname = self.handle_package(lifecycle.prime_dir, bases_config) emit.message(f"Created '{zipname}'.", intermediate=True) return zipname
def __init__( self, all_parts: Dict[str, Any], *, work_dir: pathlib.Path, assets_dir: pathlib.Path, base: str, package_repositories: List[Dict[str, Any]], parallel_build_count: int, part_names: Optional[List[str]], adopt_info: Optional[str], parse_info: Dict[str, List[str]], project_name: str, project_vars: Dict[str, str], extra_build_snaps: Optional[List[str]] = None, target_arch: str, ): self._work_dir = work_dir self._assets_dir = assets_dir self._package_repositories = package_repositories self._part_names = part_names self._adopt_info = adopt_info self._parse_info = parse_info self._all_part_names = [*all_parts] emit.progress("Initializing parts lifecycle") # set the cache dir for parts package management cache_dir = BaseDirectory.save_cache_path("snapcraft") if target_arch == "all": target_arch = get_host_architecture() platform_arch = convert_architecture_deb_to_platform(target_arch) try: self._lcm = craft_parts.LifecycleManager( {"parts": all_parts}, application_name="snapcraft", work_dir=work_dir, cache_dir=cache_dir, arch=platform_arch, base=base, ignore_local_sources=["*.snap"], extra_build_snaps=extra_build_snaps, parallel_build_count=parallel_build_count, project_name=project_name, project_vars_part_name=adopt_info, project_vars=project_vars, ) except craft_parts.PartsError as err: raise errors.PartsLifecycleError(str(err)) from err
def clean(self, *, part_names: Optional[List[str]] = None) -> None: """Remove lifecycle artifacts. :param part_names: The names of the parts to clean. If not specified, all parts will be cleaned. """ if part_names: message = "Cleaning parts: " + ", ".join(part_names) else: message = "Cleaning all parts" emit.progress(message) self._lcm.clean(part_names=part_names)
def _finalize_icon( icon: Optional[str], *, assets_dir: Path, gui_dir: Path, prime_dir: Path ) -> Optional[Path]: """Ensure sure icon is properly configured and installed. Fetch from a remote URL, if required, and place in the meta/gui directory. """ emit.debug(f"finalize icon: {icon!r}") # Nothing to do if no icon is configured, search for existing icon. if icon is None: return _find_icon_file(assets_dir) # Extracted appstream icon paths will either: # (1) point to a file relative to prime # (2) point to a remote http(s) url # # The 'icon' specified in the snapcraft.yaml has the same # constraint as (2) and would have already been validated # as existing by the schema. So we can treat it the same # at this point, regardless of the source of the icon. parsed_url = urllib.parse.urlparse(icon) parsed_path = Path(parsed_url.path) icon_ext = parsed_path.suffix[1:] target_icon_path = Path(gui_dir, f"icon.{icon_ext}") target_icon_path.parent.mkdir(parents=True, exist_ok=True) if parsed_url.scheme in ["http", "https"]: # Remote - fetch URL and write to target. emit.progress(f"Fetching icon from {icon!r}") icon_data = requests.get(icon).content target_icon_path.write_bytes(icon_data) elif parsed_url.scheme == "": source_path = Path( prime_dir, parsed_path.relative_to("/") if parsed_path.is_absolute() else parsed_path, ) if source_path.exists(): # Local with path relative to prime. _copy_file(source_path, target_icon_path) elif parsed_path.exists(): # Local with path relative to project. _copy_file(parsed_path, target_icon_path) else: # No icon found, fall back to searching for existing icon. return _find_icon_file(assets_dir) else: raise RuntimeError(f"Unexpected icon path: {parsed_url!r}") return target_icon_path
def process_version(version: Optional[str]) -> str: """Handle special version strings.""" if version is None: raise ValueError("version cannot be None") new_version = version if version == "git": emit.progress("Determining the version from the project repo (version: git).") new_version = GitSource.generate_version() if new_version != version: emit.progress(f"Version has been set to {new_version!r}", permanent=True) return new_version
def handle_package(self, prime_dir, bases_config: BasesConfiguration): """Handle the final package creation.""" emit.progress("Creating the package itself") zipname = format_charm_file_name(self.metadata.name, bases_config) zipfh = zipfile.ZipFile(zipname, "w", zipfile.ZIP_DEFLATED) for dirpath, dirnames, filenames in os.walk(prime_dir, followlinks=True): dirpath = pathlib.Path(dirpath) for filename in filenames: filepath = dirpath / filename zipfh.write(str(filepath), str(filepath.relative_to(prime_dir))) zipfh.close() return zipname
def run(self, parsed_args): # dest does not work when filling the parser so getattr instead snap_name = getattr(parsed_args, "snap-name") if parsed_args.private: emit.progress( _MESSAGE_REGISTER_PRIVATE.format(snap_name), permanent=True, ) if parsed_args.yes or utils.confirm_with_user( _MESSAGE_REGISTER_CONFIRM.format(snap_name)): store.StoreClientCLI().register(snap_name, is_private=parsed_args.private, store_id=parsed_args.store_id) emit.message(_MESSAGE_REGISTER_SUCCESS.format(snap_name)) else: emit.message(_MESSAGE_REGISTER_NO.format(snap_name))
def plan( self, *, bases_indices: Optional[List[int]], destructive_mode: bool, managed_mode: bool ) -> List[Tuple[BasesConfiguration, Base, int, int]]: """Determine the build plan based on user inputs and host environment. Provide a list of bases that are buildable and scoped according to user configuration. Provide all relevant details including the applicable bases configuration and the indices of the entries to build for. :returns: List of Tuples (bases_config, build_on, bases_index, build_on_index). """ build_plan: List[Tuple[BasesConfiguration, Base, int, int]] = [] for bases_index, bases_config in enumerate(self.config.bases): if bases_indices and bases_index not in bases_indices: emit.trace( f"Skipping 'bases[{bases_index:d}]' due to --base-index usage." ) continue for build_on_index, build_on in enumerate(bases_config.build_on): if managed_mode or destructive_mode: matches, reason = check_if_base_matches_host(build_on) else: matches, reason = self.provider.is_base_available(build_on) if matches: emit.trace( f"Building for 'bases[{bases_index:d}]' " f"as host matches 'build-on[{build_on_index:d}]'.", ) build_plan.append( (bases_config, build_on, bases_index, build_on_index)) break else: emit.progress( f"Skipping 'bases[{bases_index:d}].build-on[{build_on_index:d}]': " f"{reason}.", ) else: emit.message( "No suitable 'build-on' environment found " f"in 'bases[{bases_index:d}]' configuration.", intermediate=True, ) return build_plan
def run(self, parsed_args): snap_channel_map = store.StoreClientCLI().get_channel_map( snap_name=parsed_args.name ) existing_architectures = snap_channel_map.get_existing_architectures() if not snap_channel_map.channel_map: emit.message("This snap has no released revisions") return architectures = existing_architectures if parsed_args.arch: architectures = set(parsed_args.arch) for architecture in architectures.copy(): if architecture not in existing_architectures: emit.progress(f"No revisions for architecture {architecture!r}") architectures.remove(architecture) # If we have no revisions for any of the architectures requested, there's # nothing to do here. if not architectures: return tracks: List[str] = [] if parsed_args.track: tracks = cast(list, parsed_args.track) existing_tracks = { s.track for s in snap_channel_map.snap.channels if s.track in tracks } for track in set(tracks) - existing_tracks: emit.progress(f"No revisions for track {track!r}") tracks = list(existing_tracks) # If we have no revisions in any of the tracks requested, there's # nothing to do here. if not tracks: return emit.message( get_tabulated_channel_map( snap_channel_map, architectures=list(architectures), tracks=tracks, ) )
def error_decorator(self, *args, **kwargs): """Handle craft-store error situations and login scenarios.""" try: return method(self, *args, **kwargs) except craft_store.errors.NotLoggedIn: emit.progress("Credentials not found. Trying to log in...") except craft_store.errors.StoreServerError as error: if error.response.status_code == 401: emit.progress("Existing credentials no longer valid. Trying to log in...") else: raise CommandError(str(error)) from error except craft_store.errors.CraftStoreError as error: raise CommandError( f"Server error while communicating to the Store: {error!s}" ) from error self.login() return method(self, *args, **kwargs)
def run(self, parsed_args): if parsed_args.experimental_login: raise ArgumentParsingError( "--experimental-login no longer supported. " f"Set {store.constants.ENVIRONMENT_STORE_AUTH}=candid instead", ) if parsed_args.login_with: config_content = _read_config(parsed_args.login_with) emit.progress( "--with is no longer supported, export the auth to the environment " f"variable {store.constants.ENVIRONMENT_STORE_CREDENTIALS!r} instead", permanent=True, ) store.LegacyUbuntuOne.store_credentials(config_content) else: store.StoreClientCLI().login() emit.message("Login successful")
def handle_dependencies(self): """Handle from-directory and virtualenv dependencies.""" emit.trace("Handling dependencies") if not (self.requirement_paths or self.binary_python_packages or self.python_packages): emit.trace("No dependencies to handle") return staging_venv_dir = self.charmdir / STAGING_VENV_DIRNAME hash_file = self.charmdir / DEPENDENCIES_HASH_FILENAME # find out if current dependencies are the same than the last run. current_deps_hash = self._calculate_dependencies_hash() emit.trace(f"Current dependencies hash: {current_deps_hash!r}") if not staging_venv_dir.exists(): emit.trace("Dependencies directory not found") same_dependencies = False elif hash_file.exists(): try: previous_deps_hash = hash_file.read_text(encoding="utf8") except Exception as exc: emit.trace(f"Problems reading the dependencies hash file: {exc}") same_dependencies = False else: emit.trace(f"Previous dependencies hash: {previous_deps_hash!r}") same_dependencies = previous_deps_hash == current_deps_hash else: emit.trace("Dependencies hash file not found") same_dependencies = False if same_dependencies: emit.trace("Reusing installed dependencies, they are equal to last run ones") else: emit.progress("Installing dependencies") self._install_dependencies(staging_venv_dir) # save the hash file after all successful installations hash_file.write_text(current_deps_hash, encoding="utf8") # always copy the virtualvenv site-packages directory to /venv in charm basedir = pathlib.Path(STAGING_VENV_DIRNAME) site_packages_dir = _find_venv_site_packages(basedir) shutil.copytree(site_packages_dir, self.buildpath / VENV_DIRNAME)