Skip to content
9 changes: 5 additions & 4 deletions constructor/briefcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,9 @@ def _setup_envs_commands(info: dict) -> list[dict]:
for env_name in info.get("_extra_envs_info", {}):
env_config = info["extra_envs"][env_name]
# Needed for shortcuts_flags function
if "_conda_exe_type" not in env_config:
env_config["_conda_exe_type"] = info.get("_conda_exe_type")
if "_conda_exe" not in env_config:
env_config["_conda_exe"] = {}
env_config["_conda_exe"]["type"] = info.get("_conda_exe", {}).get("type")
channel_info = {
"channels": env_config.get("channels", info.get("channels", ())),
"channels_remap": env_config.get("channels_remap", info.get("channels_remap", ())),
Expand Down Expand Up @@ -625,14 +626,14 @@ def _stage_user_scripts(self, pkgs_dir: Path) -> None:
shutil.copy(script_path, pkgs_dir / dest_name)

def _stage_conda(self, external_dir: Path) -> None:
copy_conda_exe(external_dir, self.conda_exe_name, self.info["_conda_exe"])
copy_conda_exe(external_dir, self.conda_exe_name, self.info["_conda_exe"]["path"])


def create(info, verbose=False):
if not IS_WINDOWS:
raise OSError(f"Invalid platform '{sys.platform}'. MSI installers require Windows.")

if not info.get("_conda_exe_supports_logging"):
if not info.get("_conda_exe", {}).get("supports_logging"):
raise ValueError("MSI installers require conda-standalone with logging support.")

# Check briefcase exists before doing any work
Expand Down
2 changes: 1 addition & 1 deletion constructor/docker_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def generate_dockerfile(info: dict, docker_dir: Path) -> Path:
base_image=info.get("docker_base_image"),
default_prefix=info.get("default_prefix", f"/opt/{info['name'].lower()}"),
installer_filename=Path(info["_outpath"]).name,
conda_exe_name=format_conda_exe_name(info["_conda_exe"]),
conda_exe_name=format_conda_exe_name(info["_conda_exe"]["path"]),
name=info["name"],
version=info["version"],
labels=info.get("docker_labels", {}),
Expand Down
25 changes: 15 additions & 10 deletions constructor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from . import __version__
from ._schema import InstallerTypes
from .build_outputs import process_build_outputs
from .build_outputs import _validate_output, process_build_outputs
from .conda_interface import SUPPORTED_PLATFORMS, cc_platform
from .conda_interface import VersionOrder as Version
from .construct import SCHEMA_PATH, ns_platform
Expand All @@ -37,6 +37,7 @@
StandaloneExe,
check_version,
has_docker_buildx,
hash_files,
identify_conda_exe,
normalize_path,
yield_lines,
Expand Down Expand Up @@ -246,7 +247,7 @@ def main_build(
info["_output_dir"] = output_dir
info["_platform"] = platform
info["_download_dir"] = join(cache_dir, platform)
info["_conda_exe"] = abspath(conda_exe)
info["_conda_exe"] = {"path": abspath(conda_exe)}
info["_debug"] = debug
if installer_type:
info["installer_type"] = installer_type
Expand Down Expand Up @@ -274,11 +275,15 @@ def main_build(
):
sys.exit("Error: cannot construct a macOS 'pkg' installer on '%s'" % cc_platform)

exe_type, exe_version = identify_conda_exe(info.get("_conda_exe"))
exe_path = info["_conda_exe"]["path"]
exe_type, exe_version = identify_conda_exe(exe_path)
if exe_version is not None:
exe_version = Version(exe_version)
info["_conda_exe_type"] = exe_type
info["_conda_exe_version"] = exe_version
info["_conda_exe"]["type"] = exe_type
info["_conda_exe"]["version"] = exe_version
if any("info.json" in _validate_output(o) for o in info.get("build_outputs", ())):
info["_conda_exe"]["sha256"] = hash_files([Path(exe_path)], "sha256")["sha256"]

if osname == "win" and exe_type == StandaloneExe.MAMBA:
# TODO: Investigate errors on Windows and re-enable
sys.exit("Error: micromamba is not supported on Windows installers.")
Expand Down Expand Up @@ -397,9 +402,9 @@ def main_build(
else:
info["_ignore_condarcs_arg"] = ""

info["_conda_exe_supports_logging"] = _conda_exe_supports_logging(
info["_conda_exe"],
info["_conda_exe_type"],
info["_conda_exe"]["supports_logging"] = _conda_exe_supports_logging(
exe_path,
exe_type,
)

if InstallerTypes.PKG in itypes:
Expand All @@ -422,8 +427,8 @@ def main_build(

if osname == "win":
info["_win_install_needs_python_exe"] = _win_install_needs_python_exe(
info["_conda_exe"],
info["_conda_exe_type"],
exe_path,
exe_type,
)

info["installer_type"] = itypes[0]
Expand Down
6 changes: 3 additions & 3 deletions constructor/osxpkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def move_script(src, dst, info, ensure_shebang=False, user_script_type=None):
variables["no_rcs_arg"] = info.get("_ignore_condarcs_arg", "")
variables["script_env_variables"] = info.get("script_env_variables", {})
variables["initialize_conda"] = info.get("initialize_conda", "classic")
variables["conda_exe_name"] = format_conda_exe_name(info["_conda_exe"])
variables["conda_exe_name"] = format_conda_exe_name(info["_conda_exe"]["path"])

data = render_template(data, **variables)

Expand Down Expand Up @@ -636,8 +636,8 @@ def create(info, verbose=False):
for dist in all_dists:
os.link(join(CACHE_DIR, dist), join(pkgs_dir, dist))

exe_name = format_conda_exe_name(info["_conda_exe"])
copy_conda_exe(prefix, exe_name, info["_conda_exe"])
exe_name = format_conda_exe_name(info["_conda_exe"]["path"])
copy_conda_exe(prefix, exe_name, info["_conda_exe"]["path"])

# Sign conda-standalone so it can pass notarization
codesigner = None
Expand Down
14 changes: 7 additions & 7 deletions constructor/shar.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def get_header(conda_exec, tarball, info):
variables["default_prefix"] = info.get("default_prefix", "${HOME:-/opt}/%s" % name.lower())
variables["first_payload_size"] = getsize(conda_exec)
variables["second_payload_size"] = getsize(tarball)
variables["conda_exe_payloads"] = info.get("_conda_exe_payloads", {})
variables["conda_exe_payloads_size"] = info.get("_conda_exe_payloads_size", 0)
variables["conda_exe_payloads"] = info["_conda_exe"].get("payloads", {})
variables["conda_exe_payloads_size"] = info["_conda_exe"].get("payloads_size", 0)
variables["final_channels"] = get_final_channels(info)
variables["conclusion_text"] = info.get("conclusion_text", "installation finished.")
variables["pycache"] = "__pycache__"
Expand All @@ -112,7 +112,7 @@ def get_header(conda_exec, tarball, info):
virtual_specs = parse_virtual_specs(info)
min_osx_version = virtual_specs.get("__osx", {}).get("min") or ""
variables["min_osx_version"] = min_osx_version
variables["conda_exe_name"] = format_conda_exe_name(info["_conda_exe"])
variables["conda_exe_name"] = format_conda_exe_name(info["_conda_exe"]["path"])
min_glibc_version = virtual_specs.get("__glibc", {}).get("min") or ""
variables["min_glibc_version"] = min_glibc_version

Expand Down Expand Up @@ -210,7 +210,7 @@ def create(info, verbose=False):
t.add(join(info["_download_dir"], fn), "pkgs/" + fn)
t.close()

info["_internal_conda_files"] = copy_conda_exe(tmp_dir, "_conda", info["_conda_exe"])
info["_internal_conda_files"] = copy_conda_exe(tmp_dir, "_conda", info["_conda_exe"]["path"])
if info["_internal_conda_files"]:
conda_exe_payloads: dict[str, tuple[int, int, bool]] = {}
memfile = BytesIO()
Expand All @@ -225,13 +225,13 @@ def create(info, verbose=False):
conda_exe_payloads[relative_path] = (start, end, executable)
start = end

info["_conda_exe_payloads"] = conda_exe_payloads
info["_conda_exe_payloads_size"] = end
info["_conda_exe"]["payloads"] = conda_exe_payloads
info["_conda_exe"]["payloads_size"] = end
memfile.seek(0)
maybe_memfile = (memfile,)
else:
maybe_memfile = ()
conda_exec = info["_conda_exe"]
conda_exec = info["_conda_exe"]["path"]
header = get_header(conda_exec, tarball, info)
shar_path = info["_outpath"]
with open(shar_path, "wb") as fo:
Expand Down
4 changes: 2 additions & 2 deletions constructor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def ensure_transmuted_ext(info, url):
"""
if (
info.get("transmute_file_type") == ".conda"
and info.get("_conda_exe_type") == StandaloneExe.MAMBA
and info.get("_conda_exe", {}).get("type") == StandaloneExe.MAMBA
):
if url.lower().endswith(".tar.bz2"):
url = url[:-8] + ".conda"
Expand Down Expand Up @@ -307,7 +307,7 @@ def shortcuts_flags(info) -> str:
# not set: we create all shortcuts (default behaviour)
return ""
if menu_packages:
if info.get("_conda_exe_type") == StandaloneExe.MAMBA:
if info.get("_conda_exe", {}).get("type") == StandaloneExe.MAMBA:
logger.warning(
"Micromamba does not support '--shortcuts-only'. Will install all shortcuts."
)
Expand Down
9 changes: 5 additions & 4 deletions constructor/winexe.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ def setup_envs_commands(info, dir_path):
for env_name in info.get("_extra_envs_info", {}):
env_info = info["extra_envs"][env_name]
# Needed for shortcuts_flags function
if "_conda_exe_type" not in env_info:
env_info["_conda_exe_type"] = info.get("_conda_exe_type")
if "_conda_exe" not in env_info:
env_info["_conda_exe"] = {}
env_info["_conda_exe"]["type"] = info.get("_conda_exe", {}).get("type")
channel_info = {
"channels": env_info.get("channels", info.get("channels", ())),
"channels_remap": env_info.get("channels_remap", info.get("channels_remap", ())),
Expand Down Expand Up @@ -282,7 +283,7 @@ def make_nsi(

# UPPERCASE variables are unescaped (and unquoted)
variables["CONDA_LOG_ARG"] = (
'--log-file "${STEP_LOG}"' if info.get("_conda_exe_supports_logging") else ""
'--log-file "${STEP_LOG}"' if info.get("_conda_exe", {}).get("supports_logging") else ""
)
variables["NAME"] = name
variables["NSIS_DIR"] = NSIS_DIR
Expand Down Expand Up @@ -376,7 +377,7 @@ def create(info, verbose=False):
preconda_write_files(info, tmp_dir)
copied_extra_files = copy_extra_files(info.get("extra_files", []), tmp_dir)
copied_temp_extra_files = copy_extra_files(info.get("temp_extra_files", []), tmp_dir)
extra_conda_exe_files = copy_conda_exe(tmp_dir, "_conda.exe", info["_conda_exe"])
extra_conda_exe_files = copy_conda_exe(tmp_dir, "_conda.exe", info["_conda_exe"]["path"])

pre_dst = join(tmp_dir, "pre_install.bat")
pre_install_script = info.get("pre_install")
Expand Down
19 changes: 19 additions & 0 deletions news/1329-conda-standalone-metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Add SHA256 hash of bootstrapper to `info.json` and consolidate bootstrapper metadata under `_conda_exe`, including its hash, type, version, and path. (#1329)

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
2 changes: 1 addition & 1 deletion tests/test_briefcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
mock_info = {
"name": "MockInfo",
"version": "1.0.0",
"_conda_exe": str(Path(sys.prefix) / "standalone_conda" / "conda.exe"),
"_conda_exe": {"path": str(Path(sys.prefix) / "standalone_conda" / "conda.exe")},
"_download_dir": "",
"_dists": [],
"_platform": cc_platform,
Expand Down
10 changes: 9 additions & 1 deletion tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -2005,7 +2005,15 @@ def test_output_files(tmp_path, installer_type):

# Test that info.json contains serialized objects
info_json = json.loads((root_path / "info.json").read_text())
assert isinstance(info_json.get("_conda_exe_version"), str)
conda_exe = info_json["_conda_exe"]

assert isinstance(conda_exe.get("version"), str) and conda_exe["version"]
assert isinstance(conda_exe.get("path"), str) and conda_exe["path"]
assert isinstance(conda_exe.get("type"), str) and conda_exe["type"]

expected_sha256 = hashlib.sha256(Path(conda_exe["path"]).read_bytes()).hexdigest()
assert conda_exe["sha256"] == expected_sha256

_build_environment_packages = info_json.get("_build_environment_packages")
assert isinstance(_build_environment_packages, list), (
"Build environment packages is not a list."
Expand Down
Loading