Skip to content

Commit

Permalink
Merge branch 'main' into nsis-log-stdout
Browse files Browse the repository at this point in the history
  • Loading branch information
jaimergp committed Sep 10, 2024
2 parents 73d333c + a4a8ffe commit 691f134
Show file tree
Hide file tree
Showing 24 changed files with 958 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ jobs:
git diff --exit-code
- name: Upload the example installers as artifacts
if: github.event_name == 'pull_request' && matrix.python-version == '3.9'
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: installers-${{ runner.os }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
path: "${{ runner.temp }}/examples_artifacts"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
- if: github.event.comment.body != '@conda-bot render'
id: create
# no-op if no commits were made
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
uses: peter-evans/create-pull-request@8867c4aba1b742c39f8d0ba35429c2dfa4b6cb20 # v7.0.1
with:
push-to-fork: ${{ env.FORK }}
token: ${{ secrets.SYNC_TOKEN }}
Expand Down
18 changes: 15 additions & 3 deletions CONSTRUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,20 @@ If this key is missing, it defaults to a message about Anaconda Cloud.
You can disable it altogether if you set this key to `""` (empty string).
(MacOS only).

### `post_install_pages`

_required:_ no<br/>
_types:_ list, string<br/>

Adds extra pages to the installers to be shown after installation.

For PKG installers, these can be compiled `installer` plug-ins or
directories containing an Xcode project. In the latter case,
constructor will try and compile the project file using `xcodebuild`.

For Windows, the extra pages must be `.nsi` files.
They will be inserted as-is before the conclusion page.

### `conclusion_file`

_required:_ no<br/>
Expand All @@ -788,9 +802,7 @@ plain text (.txt), rich text (.rtf) or HTML (.html). If both
`conclusion_file` and `conclusion_text` are provided,
`conclusion_file` takes precedence. (MacOS only).

If the installer is for windows and conclusion file type is nsi,
it will use the nsi script to add in extra pages and the conclusion file
at the end of the installer.
If the installer is for Windows, the file type must be nsi.

### `conclusion_text`

Expand Down
15 changes: 12 additions & 3 deletions constructor/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,17 @@
If this key is missing, it defaults to a message about Anaconda Cloud.
You can disable it altogether if you set this key to `""` (empty string).
(MacOS only).
'''),

('post_install_pages', False, (list, str), '''
Adds extra pages to the installers to be shown after installation.
For PKG installers, these can be compiled `installer` plug-ins or
directories containing an Xcode project. In the latter case,
constructor will try and compile the project file using `xcodebuild`.
For Windows, the extra pages must be `.nsi` files.
They will be inserted as-is before the conclusion page.
'''),

('conclusion_file', False, str, '''
Expand All @@ -578,9 +589,7 @@
`conclusion_file` and `conclusion_text` are provided,
`conclusion_file` takes precedence. (MacOS only).
If the installer is for windows and conclusion file type is nsi,
it will use the nsi script to add in extra pages and the conclusion file
at the end of the installer.
If the installer is for Windows, the file type must be nsi.
'''),

('conclusion_text', False, str, '''
Expand Down
9 changes: 6 additions & 3 deletions constructor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,12 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
for key in ('license_file', 'welcome_image', 'header_image', 'icon_image',
'pre_install', 'post_install', 'pre_uninstall', 'environment_file',
'nsis_template', 'welcome_file', 'readme_file', 'conclusion_file',
'signing_certificate'):
if info.get(key): # only join if there's a truthy value set
info[key] = abspath(join(dir_path, info[key]))
'signing_certificate', 'post_install_pages'):
if value := info.get(key): # only join if there's a truthy value set
if isinstance(value, str):
info[key] = abspath(join(dir_path, info[key]))
elif isinstance(value, list):
info[key] = [abspath(join(dir_path, val)) for val in value]

# Normalize name and set default value
if info.get("windows_signing_tool"):
Expand Down
5 changes: 5 additions & 0 deletions constructor/nsis/main.nsi.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ Page Custom InstModePage_Create InstModePage_Leave
# Custom options now differ depending on installation mode.
Page Custom mui_AnaCustomOptions_Show
!insertmacro MUI_PAGE_INSTFILES

#if post_install_pages is True
@POST_INSTALL_PAGES@
#endif

#if with_conclusion_text is True
!define MUI_FINISHPAGE_TITLE __CONCLUSION_TITLE__
!define MUI_FINISHPAGE_TITLE_3LINES
Expand Down
121 changes: 97 additions & 24 deletions constructor/osxpkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
import os
import shlex
import shutil
import subprocess
import sys
import xml.etree.ElementTree as ET
from os.path import abspath, dirname, exists, isdir, join
from pathlib import Path
from plistlib import dump as plist_dump
from tempfile import NamedTemporaryFile
from typing import List

from . import preconda
from .conda_interface import conda_context
from .construct import ns_platform, parse
from .imaging import write_images
from .signing import CodeSign
from .utils import (
add_condarc,
approx_size_kb,
Expand Down Expand Up @@ -423,6 +426,80 @@ def pkgbuild_prepare_installation(info):
shutil.rmtree(f"{pkg}.expanded")


def create_plugins(pages: list = None, codesigner: CodeSign = None):
def _build_xcode_projects(xcodeporj_dirs: List[Path]):
xcodebuild = shutil.which("xcodebuild")
if not xcodebuild:
raise RuntimeError(
"Plugin directory contains an uncompiled project,"
" but xcodebuild is not available."
)
try:
subprocess.run([xcodebuild, "--help"], check=True, capture_output=True)
except subprocess.CalledSubprocessError:
raise RuntimeError(
"Plugin directory contains an uncompiled project, "
"but xcodebuild requires XCode to compile plugins."
)
for xcodeproj in xcodeproj_dirs:
build_cmd = [
xcodebuild,
"-project",
str(xcodeproj),
f"CONFIGURATION_BUILD_DIR={PLUGINS_DIR}",
# do not create dSYM debug symbols directory
"DEBUG_INFORMATION_FORMAT=",
]
explained_check_call(build_cmd)

if not pages:
return
elif isinstance(pages, str):
pages = [pages]

fresh_dir(PLUGINS_DIR)

for page in pages:
xcodeproj_dirs = [
file.resolve()
for file in Path(page).iterdir()
if file.suffix == ".xcodeproj"
]
if xcodeproj_dirs:
_build_xcode_projects(xcodeproj_dirs)
else:
plugin_name = os.path.basename(page)
page_in_plugins = join(PLUGINS_DIR, plugin_name)
shutil.copytree(page, page_in_plugins)

if codesigner:
with NamedTemporaryFile(suffix=".plist", delete=False) as entitlements:
plist = {
"com.apple.security.cs.allow-unsigned-executable-memory": True,
"com.apple.security.cs.disable-library-validation": True,
}
plist_dump(plist, entitlements)

for path in Path(PLUGINS_DIR).iterdir():
codesigner.sign_bundle(path, entitlements=entitlements.name)
os.unlink(entitlements.name)

plugins = [file.name for file in Path(PLUGINS_DIR).iterdir()]
with open(join(PLUGINS_DIR, "InstallerSections.plist"), "wb") as f:
plist = {
"SectionOrder": [
"Introduction",
"ReadMe",
"License",
"Target",
"PackageSelection",
"Install",
*plugins,
]
}
plist_dump(plist, f)


def pkgbuild_script(name, info, src, dst='postinstall', **kwargs):
fresh_dir(SCRIPTS_DIR)
fresh_dir(PACKAGE_ROOT)
Expand All @@ -446,12 +523,13 @@ def create(info, verbose=False):
"installation! Aborting!"
)

global CACHE_DIR, PACKAGE_ROOT, PACKAGES_DIR, SCRIPTS_DIR
global CACHE_DIR, PACKAGE_ROOT, PACKAGES_DIR, PLUGINS_DIR, SCRIPTS_DIR

CACHE_DIR = info['_download_dir']
SCRIPTS_DIR = join(CACHE_DIR, "scripts")
PACKAGE_ROOT = join(CACHE_DIR, "package_root")
PACKAGES_DIR = join(CACHE_DIR, "built_pkgs")
PLUGINS_DIR = join(CACHE_DIR, "plugins")

fresh_dir(PACKAGES_DIR)
prefix = join(PACKAGE_ROOT, info.get("pkg_name", info['name']).lower())
Expand Down Expand Up @@ -499,31 +577,20 @@ def create(info, verbose=False):
shutil.copyfile(info['_conda_exe'], join(prefix, "_conda"))

# Sign conda-standalone so it can pass notarization
notarization_identity_name = info.get('notarization_identity_name')
if notarization_identity_name:
with NamedTemporaryFile(suffix=".plist", delete=False) as f:
plist = {
codesigner = None
if notarization_identity_name := info.get('notarization_identity_name'):
codesigner = CodeSign(
notarization_identity_name,
prefix=info.get("reverse_domain_identifier", info['name'])
)
entitlements = {
"com.apple.security.cs.allow-jit": True,
"com.apple.security.cs.allow-unsigned-executable-memory": True,
"com.apple.security.cs.disable-executable-page-protection": True,
"com.apple.security.cs.disable-library-validation": True,
"com.apple.security.cs.allow-dyld-environment-variables": True,
}
plist_dump(plist, f)
explained_check_call(
[
# hardcode to system location to avoid accidental clobber in PATH
"/usr/bin/codesign",
"--verbose",
'--sign', notarization_identity_name,
"--prefix", info.get("reverse_domain_identifier", info['name']),
"--options", "runtime",
"--force",
"--entitlements", f.name,
join(prefix, "_conda"),
]
)
os.unlink(f.name)
}
codesigner.sign_bundle(join(prefix, "_conda"), entitlements=entitlements)

# This script checks to see if the install location already exists and/or contains spaces
# Not to be confused with the user-provided pre_install!
Expand Down Expand Up @@ -579,14 +646,20 @@ def create(info, verbose=False):
explained_check_call(args)
modify_xml(xml_path, info)

if plugins := info.get("post_install_pages"):
create_plugins(plugins, codesigner=codesigner)

identity_name = info.get('signing_identity_name')
explained_check_call([
build_cmd = [
"/usr/bin/productbuild",
"--distribution", xml_path,
"--package-path", PACKAGES_DIR,
"--identifier", info.get("reverse_domain_identifier", info['name']),
"tmp.pkg" if identity_name else info['_outpath']
])
]
if plugins:
build_cmd.extend(["--plugins", PLUGINS_DIR])
build_cmd.append("tmp.pkg" if identity_name else info['_outpath'])
explained_check_call(build_cmd)
if identity_name:
explained_check_call([
# hardcode to system location to avoid accidental clobber in PATH
Expand Down
53 changes: 52 additions & 1 deletion constructor/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
import os
import shutil
from pathlib import Path
from plistlib import dump as plist_dump
from subprocess import PIPE, STDOUT, check_call, run
from tempfile import NamedTemporaryFile
from typing import Union

from .utils import check_required_env_vars, win_str_esc
from .utils import check_required_env_vars, explained_check_call, win_str_esc

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -218,3 +220,52 @@ def verify_signature(self, installer_file: Union[str, Path]):
except ValueError:
# Something else is in the output
raise RuntimeError(f"Unexpected signature verification output: {proc.stdout}")


class CodeSign(SigningTool):
def __init__(
self,
identity_name: str,
prefix: str = None,
):
# hardcode to system location to avoid accidental clobber in PATH
super().__init__("/usr/bin/codesign")
self.identity_name = identity_name
self.prefix = prefix

def get_signing_command(
self,
bundle: Union[str, Path],
entitlements: Union[str, Path] = None,
) -> list:
command = [
self.executable,
"--sign",
self.identity_name,
"--force",
"--options",
"runtime",
]
if self.prefix:
command.extend(["--prefix", self.prefix])
if entitlements:
command.extend(["--entitlements", str(entitlements)])
if logger.getEffectiveLevel() == logging.DEBUG:
command.append("--verbose")
command.append(str(bundle))
return command

def sign_bundle(
self,
bundle: Union[str, Path],
entitlements: Union[str, Path, dict] = None,
):
if isinstance(entitlements, dict):
with NamedTemporaryFile(suffix=".plist", delete=False) as ent_file:
plist_dump(entitlements, ent_file)
command = self.get_signing_command(bundle, entitlements=ent_file.name)
explained_check_call(command)
os.unlink(ent_file.name)
else:
command = self.get_signing_command(bundle, entitlements=entitlements)
explained_check_call(command)
Loading

0 comments on commit 691f134

Please sign in to comment.