-
Notifications
You must be signed in to change notification settings - Fork 176
Briefcase installer options #1144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: briefcase-integration
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,200 @@ | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| Logic to build installers using Briefcase. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||
| import shutil | ||||||||||||||||||||||||||
| import sysconfig | ||||||||||||||||||||||||||
| import tempfile | ||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||
| from subprocess import run | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import tomli_w | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| from . import preconda | ||||||||||||||||||||||||||
| from .utils import DEFAULT_REVERSE_DOMAIN_ID, copy_conda_exe, filename_dist | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| BRIEFCASE_DIR = Path(__file__).parent / "briefcase" | ||||||||||||||||||||||||||
| EXTERNAL_PACKAGE_PATH = "external" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Default to a low version, so that if a valid version is provided in the future, it'll | ||||||||||||||||||||||||||
| # be treated as an upgrade. | ||||||||||||||||||||||||||
| DEFAULT_VERSION = "0.0.1" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def get_name_version(info): | ||||||||||||||||||||||||||
| if not (name := info.get("name")): | ||||||||||||||||||||||||||
| raise ValueError("Name is empty") | ||||||||||||||||||||||||||
| if not (version := info.get("version")): | ||||||||||||||||||||||||||
| raise ValueError("Version is empty") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Briefcase requires version numbers to be in the canonical Python format, and some | ||||||||||||||||||||||||||
| # installer types use the version to distinguish between upgrades, downgrades and | ||||||||||||||||||||||||||
| # reinstalls. So try to produce a consistent ordering by extracting the last valid | ||||||||||||||||||||||||||
| # version from the Constructor version string. | ||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||
| # Hyphens aren't allowed in this format, but for compatibility with Miniconda's | ||||||||||||||||||||||||||
| # version format, we treat them as dots. | ||||||||||||||||||||||||||
| matches = list( | ||||||||||||||||||||||||||
| re.finditer( | ||||||||||||||||||||||||||
| r"(\d+!)?\d+(\.\d+)*((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?", | ||||||||||||||||||||||||||
| version.lower().replace("-", "."), | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| if not matches: | ||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||
| f"Version {version!r} contains no valid version numbers; " | ||||||||||||||||||||||||||
| f"defaulting to {DEFAULT_VERSION}" | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| return f"{name} {version}", DEFAULT_VERSION | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| match = matches[-1] | ||||||||||||||||||||||||||
| version = match.group() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Treat anything else in the version string as part of the name. | ||||||||||||||||||||||||||
| start, end = match.span() | ||||||||||||||||||||||||||
| strip_chars = " .-_" | ||||||||||||||||||||||||||
| before = info["version"][:start].strip(strip_chars) | ||||||||||||||||||||||||||
| after = info["version"][end:].strip(strip_chars) | ||||||||||||||||||||||||||
| name = " ".join(s for s in [name, before, after] if s) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return name, version | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Takes an arbitrary string with at least one alphanumeric character, and makes it into | ||||||||||||||||||||||||||
| # a valid Python package name. | ||||||||||||||||||||||||||
| def make_app_name(name, source): | ||||||||||||||||||||||||||
| app_name = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") | ||||||||||||||||||||||||||
| if not app_name: | ||||||||||||||||||||||||||
| raise ValueError(f"{source} contains no alphanumeric characters") | ||||||||||||||||||||||||||
| return app_name | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Some installer types use the reverse domain ID to detect when the product is already | ||||||||||||||||||||||||||
| # installed, so it should be both unique between different products, and stable between | ||||||||||||||||||||||||||
| # different versions of a product. | ||||||||||||||||||||||||||
| def get_bundle_app_name(info, name): | ||||||||||||||||||||||||||
| # If reverse_domain_identifier is provided, use it as-is, | ||||||||||||||||||||||||||
| if (rdi := info.get("reverse_domain_identifier")) is not None: | ||||||||||||||||||||||||||
| if "." not in rdi: | ||||||||||||||||||||||||||
| raise ValueError(f"reverse_domain_identifier {rdi!r} contains no dots") | ||||||||||||||||||||||||||
| bundle, app_name = rdi.rsplit(".", 1) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Ensure that the last component is a valid Python package name, as Briefcase | ||||||||||||||||||||||||||
| # requires. | ||||||||||||||||||||||||||
| if not re.fullmatch( | ||||||||||||||||||||||||||
| r"[A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9]", app_name, flags=re.IGNORECASE | ||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||
| app_name = make_app_name( | ||||||||||||||||||||||||||
| app_name, f"Last component of reverse_domain_identifier {rdi!r}" | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # If reverse_domain_identifier isn't provided, generate it from the name. | ||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||
| bundle = DEFAULT_REVERSE_DOMAIN_ID | ||||||||||||||||||||||||||
| app_name = make_app_name(name, f"Name {name!r}") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return bundle, app_name | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def create_install_options_list(info: dict) -> list[dict]: | ||||||||||||||||||||||||||
| """Returns a list of dicts with data formatted for the installation options page.""" | ||||||||||||||||||||||||||
| options = [] | ||||||||||||||||||||||||||
| register_python = info.get("register_python", True) | ||||||||||||||||||||||||||
| if register_python: | ||||||||||||||||||||||||||
| options.append( | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| "name": "register_python", | ||||||||||||||||||||||||||
| "title": "Register Python as System Default", | ||||||||||||||||||||||||||
| "description": "TODO: Register Python description", | ||||||||||||||||||||||||||
| "default": info.get("register_python_default", False), | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| initialize_conda = info.get("initialize_conda", "classic") | ||||||||||||||||||||||||||
| if initialize_conda: | ||||||||||||||||||||||||||
| # TODO: How would we distinguish between True/classic in the UI? Same for NSIS | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| # Account for the conda mode | |
| ${If} "${INIT_CONDA_MODE}" == "condabin" | |
| ${NSD_CreateLabel} 5% "$5u" 90% 20u \ | |
| "Adds condabin/, which only contains the 'conda' executables, to PATH. \ | |
| Does not require special shortcuts but activation needs \ | |
| to be performed manually." | |
| ${Else} | |
| ${NSD_CreateLabel} 5% "$5u" 90% 20u \ | |
| "NOT recommended. This can lead to conflicts with other applications. \ | |
| Instead, use the Commmand Prompt and Powershell menus added \ | |
| to the Windows Start Menu." | |
| ${EndIf} |
You can then add the condabin UI text if info.get("initialize_conda", "classic") == "condabin", and the other text if not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, updated a642278
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| set PREFIX=%cd% | ||
| _conda constructor --prefix %PREFIX% --extract-conda-pkgs | ||
|
|
||
| set CONDA_PROTECT_FROZEN_ENVS=0 | ||
| set CONDA_ROOT_PREFIX=%PREFIX% | ||
| set CONDA_SAFETY_CHECKS=disabled | ||
| set CONDA_EXTRA_SAFETY_CHECKS=no | ||
| set CONDA_PKGS_DIRS=%PREFIX%\pkgs | ||
|
|
||
| _conda install --offline --file %PREFIX%\conda-meta\initial-state.explicit.txt -yp %PREFIX% |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,3 @@ | ||
| conda-forge::briefcase>=0.3.26 | ||
| conda-forge::nsis>=3.08=*_log_* | ||
| conda-forge::tomli-w>=1.2.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, you can just copy the description in NSIS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed 326be7f