From 2a8f0e447dcab22a8244562fbec09f47bca52cae Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 20 Aug 2024 08:49:36 -0700 Subject: [PATCH 01/35] Add extra pages feature for pkg installers --- constructor/construct.py | 5 ++++ constructor/main.py | 9 ++++-- constructor/osxpkg.py | 63 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/constructor/construct.py b/constructor/construct.py index 19b09d52d..d3a7d2336 100644 --- a/constructor/construct.py +++ b/constructor/construct.py @@ -569,6 +569,11 @@ 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 panels to the installers to be shown after installation. +For pkg installers, these must be compiled installer plug-ins. '''), ('conclusion_file', False, str, ''' diff --git a/constructor/main.py b/constructor/main.py index 78d860bfb..98e6a080c 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -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"): diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index faebf490b..f30696404 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -423,6 +423,55 @@ def pkgbuild_prepare_installation(info): shutil.rmtree(f"{pkg}.expanded") +def create_plugins(plugins_dir: str, info: dict): + + if notarization_identity_name := info.get('notarization_identity_name'): + 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) + + pages = info.get("post_install_pages", []) + if isinstance(pages, str): + pages = [pages] + + for page in pages: + page_in_plugins = join(plugins_dir, os.path.basename(page)) + shutil.copytree(page, page_in_plugins) + if notarization_identity_name: + 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", entitlements.name, + page_in_plugins, + ] + ) + if notarization_identity_name: + os.unlink(entitlements.name) + + with open(join(plugins_dir, "InstallerSections.plist"), "wb") as f: + plist = { + "SectionOrder": [ + "Introduction", + "ReadMe", + "License", + "Target", + "PackageSelection", + "Install", + *[os.path.basename(page) for page in pages] + ] + } + plist_dump(plist, f) + + def pkgbuild_script(name, info, src, dst='postinstall', **kwargs): fresh_dir(SCRIPTS_DIR) fresh_dir(PACKAGE_ROOT) @@ -579,14 +628,22 @@ def create(info, verbose=False): explained_check_call(args) modify_xml(xml_path, info) + if plugins := info.get("post_install_pages"): + plugins_dir = join(CACHE_DIR, "plugins") + fresh_dir(plugins_dir) + create_plugins(plugins_dir, info) + 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 From 62b3cc6eacab720c1facfa4ced355d5ab67bf33e Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 20 Aug 2024 12:06:59 -0700 Subject: [PATCH 02/35] Compile xcodeprojects --- constructor/construct.py | 4 +- constructor/osxpkg.py | 78 +++- examples/osxpkg_extra_pages/construct.yaml | 70 ++++ .../ExtraPage.xcodeproj/project.pbxproj | 341 ++++++++++++++++++ .../ExtraPage/Base.lproj/ExtraPage.xib | 38 ++ .../plugins/ExtraPage/ExtraPage/ExtraPage.h | 5 + .../plugins/ExtraPage/ExtraPage/ExtraPage.m | 10 + .../plugins/ExtraPage/ExtraPage/Info.plist | 8 + tests/test_examples.py | 32 ++ 9 files changed, 565 insertions(+), 21 deletions(-) create mode 100644 examples/osxpkg_extra_pages/construct.yaml create mode 100644 examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj create mode 100644 examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/Base.lproj/ExtraPage.xib create mode 100644 examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/ExtraPage.h create mode 100644 examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/ExtraPage.m create mode 100644 examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/Info.plist diff --git a/constructor/construct.py b/constructor/construct.py index d3a7d2336..9dc9e033b 100644 --- a/constructor/construct.py +++ b/constructor/construct.py @@ -573,7 +573,9 @@ ('post_install_pages', False, (list, str), ''' Adds extra panels to the installers to be shown after installation. -For pkg installers, these must be compiled installer plug-ins. +For pkg installers, these can be compiled installer plug-ins or +a directories containing an Xcode project. In the latter case, +constructor will try and compile the project file using `xcodebuild`. '''), ('conclusion_file', False, str, ''' diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index f30696404..36309a387 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -424,6 +424,13 @@ def pkgbuild_prepare_installation(info): def create_plugins(plugins_dir: str, info: dict): + pages = info.get("post_install_pages") + if not pages: + return + elif isinstance(pages, str): + pages = [pages] + + xcodebuild = shutil.which("xcodebuild") if notarization_identity_name := info.get('notarization_identity_name'): with NamedTemporaryFile(suffix=".plist", delete=False) as entitlements: @@ -433,30 +440,61 @@ def create_plugins(plugins_dir: str, info: dict): } plist_dump(plist, entitlements) - pages = info.get("post_install_pages", []) - if isinstance(pages, str): - pages = [pages] - for page in pages: - page_in_plugins = join(plugins_dir, os.path.basename(page)) - shutil.copytree(page, page_in_plugins) - if notarization_identity_name: - 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", entitlements.name, - page_in_plugins, + xcodeproj_dirs = [ + file.resolve() + for file in Path(page).iterdir() + if file.suffix == ".xcodeproj" + ] + if xcodeproj_dirs: + if not xcodebuild: + raise RuntimeError( + "Plugin directory contains an uncompiled project," + " but xcodebuild is not available." + ) + 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=", ] - ) + if notarization_identity_name: + extra_flags = ( + "--verbose" + "--prefix {info.get('reverse_domain_identifier', info['name'])}" + "--options runtime" + ) + build_cmd.extend([ + f"CODE_SIGN_IDENTITY={notarization_identity_name}", + f"CODE_SIGN_ENTITLEMENTS={entitlements.name}", + f"OTHER_CODE_SIGN_FLAGS=\"{' '.join(extra_flags)}\"", + ]) + explained_check_call(build_cmd) + else: + plugin_name = os.path.basename(page) + page_in_plugins = join(plugins_dir, plugin_name) + shutil.copytree(page, page_in_plugins) + if notarization_identity_name: + 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", entitlements.name, + page_in_plugins, + ] + ) if notarization_identity_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": [ @@ -466,7 +504,7 @@ def create_plugins(plugins_dir: str, info: dict): "Target", "PackageSelection", "Install", - *[os.path.basename(page) for page in pages] + *plugins, ] } plist_dump(plist, f) diff --git a/examples/osxpkg_extra_pages/construct.yaml b/examples/osxpkg_extra_pages/construct.yaml new file mode 100644 index 000000000..80eb29a82 --- /dev/null +++ b/examples/osxpkg_extra_pages/construct.yaml @@ -0,0 +1,70 @@ +name: osxpkgtest +version: 1.2.3 + +# This config will result in a default install path: +# "~/Library/osx-pkg-test" (spaces not allowed because 'conda' is in 'specs') +default_location_pkg: Library +pkg_name: "osx-pkg-test" + +channels: + - http://repo.anaconda.com/pkgs/main/ + +attempt_hardlinks: True + +specs: + - conda + - openssl + +installer_type: pkg # [osx] + +reverse_domain_identifier: org.website.my + +## You can provide `welcome_image` to a 1200x600 PNG file +## Ideally, transparent background with your logo on the +## bottom left corner. +# welcome_image: ../../constructor/osx/MacInstaller.png +## If you don't want any logo, disable it explicitly: +welcome_image: "" +## If you want an autogenerated logo with the package name +## and version, you need to be explicit about it: +welcome_image_text: osxpkgtest +## Note that if both `welcome_image` and `welcome_image_text` +## are provided, `welcome_image` takes precedence. + +# First screen (labeled introduction) text +# If not provided, defaults to standard message +# welcome_text: | +# something for the users +welcome_text: "" +# You can also pass a file (plain or rich text) +# welcome_file: "path/to/text.rtf" + +# Shown before the license; if not provided, it defaults +# to Anaconda's message. Set it to "" (empty string) to disable it. +# readme_text: | +# something for the users +readme_text: "" +# You can also pass a file (plain or rich text) +# readme_file: "path/to/text.rtf" + +# This will be shown at the end of the wizard +conclusion_text: | + Thanks for installing osxpkgtest v1.2.3! + +# Disable with "" (empty string) to default to the system message: +# conclusion_text: "" + +# You can also pass a file (plain or rich text) +# conclusion_file: "path/to/text.rtf" + +# signing_identity_name: "Developer ID Installer: XXX XXXX XXX (XXXXXX)" +# notarization_identity_name: "Developer ID Application: XXX XXXX XXX (XXXXXX)" + +install_path_exists_error_text: > + {CHOSEN_PATH} exists! Please update using our in-app mechanisms or + relaunch the installer and choose a different location. + +initialize_by_default: false +register_python: False + +post_install_pages: "plugins/ExtraPage" diff --git a/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj new file mode 100644 index 000000000..8cb946dd6 --- /dev/null +++ b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj @@ -0,0 +1,341 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + ABACDD4F2C73F6BD002DA78F /* ExtraPage.m in Sources */ = {isa = PBXBuildFile; fileRef = ABACDD4E2C73F6BD002DA78F /* ExtraPage.m */; }; + ABACDD522C73F6BD002DA78F /* ExtraPage.xib in Resources */ = {isa = PBXBuildFile; fileRef = ABACDD502C73F6BD002DA78F /* ExtraPage.xib */; }; + ABACDD552C73F6BD002DA78F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = ABACDD532C73F6BD002DA78F /* Localizable.strings */; }; + ABACDD5B2C73F6BD002DA78F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = ABACDD592C73F6BD002DA78F /* InfoPlist.strings */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + ABACDD4A2C73F6BD002DA78F /* ExtraPage.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExtraPage.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + ABACDD4D2C73F6BD002DA78F /* ExtraPage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExtraPage.h; sourceTree = ""; }; + ABACDD4E2C73F6BD002DA78F /* ExtraPage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExtraPage.m; sourceTree = ""; }; + ABACDD512C73F6BD002DA78F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ExtraPage.xib; sourceTree = ""; }; + ABACDD582C73F6BD002DA78F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + ABACDD472C73F6BD002DA78F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + ABACDD412C73F6BD002DA78F = { + isa = PBXGroup; + children = ( + ABACDD4C2C73F6BD002DA78F /* ExtraPage */, + ABACDD4B2C73F6BD002DA78F /* Products */, + ); + sourceTree = ""; + }; + ABACDD4B2C73F6BD002DA78F /* Products */ = { + isa = PBXGroup; + children = ( + ABACDD4A2C73F6BD002DA78F /* ExtraPage.bundle */, + ); + name = Products; + sourceTree = ""; + }; + ABACDD4C2C73F6BD002DA78F /* ExtraPage */ = { + isa = PBXGroup; + children = ( + ABACDD4D2C73F6BD002DA78F /* ExtraPage.h */, + ABACDD4E2C73F6BD002DA78F /* ExtraPage.m */, + ABACDD502C73F6BD002DA78F /* ExtraPage.xib */, + ABACDD532C73F6BD002DA78F /* Localizable.strings */, + ABACDD582C73F6BD002DA78F /* Info.plist */, + ABACDD592C73F6BD002DA78F /* InfoPlist.strings */, + ); + path = ExtraPage; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + ABACDD492C73F6BD002DA78F /* ExtraPage */ = { + isa = PBXNativeTarget; + buildConfigurationList = ABACDD5E2C73F6BD002DA78F /* Build configuration list for PBXNativeTarget "ExtraPage" */; + buildPhases = ( + ABACDD462C73F6BD002DA78F /* Sources */, + ABACDD472C73F6BD002DA78F /* Frameworks */, + ABACDD482C73F6BD002DA78F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ExtraPage; + productName = ExtraPage; + productReference = ABACDD4A2C73F6BD002DA78F /* ExtraPage.bundle */; + productType = "com.apple.product-type.bundle"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + ABACDD422C73F6BD002DA78F /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 1430; + TargetAttributes = { + ABACDD492C73F6BD002DA78F = { + CreatedOnToolsVersion = 14.3; + }; + }; + }; + buildConfigurationList = ABACDD452C73F6BD002DA78F /* Build configuration list for PBXProject "ExtraPage" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = ABACDD412C73F6BD002DA78F; + productRefGroup = ABACDD4B2C73F6BD002DA78F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + ABACDD492C73F6BD002DA78F /* ExtraPage */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + ABACDD482C73F6BD002DA78F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ABACDD522C73F6BD002DA78F /* ExtraPage.xib in Resources */, + ABACDD5B2C73F6BD002DA78F /* InfoPlist.strings in Resources */, + ABACDD552C73F6BD002DA78F /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + ABACDD462C73F6BD002DA78F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ABACDD4F2C73F6BD002DA78F /* ExtraPage.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + ABACDD502C73F6BD002DA78F /* ExtraPage.xib */ = { + isa = PBXVariantGroup; + children = ( + ABACDD512C73F6BD002DA78F /* Base */, + ); + name = ExtraPage.xib; + sourceTree = ""; + }; + ABACDD532C73F6BD002DA78F /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + ABACDD542C73F6BD002DA78F /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + ABACDD592C73F6BD002DA78F /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + ABACDD5A2C73F6BD002DA78F /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + ABACDD5C2C73F6BD002DA78F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + ABACDD5D2C73F6BD002DA78F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + }; + name = Release; + }; + ABACDD5F2C73F6BD002DA78F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ExtraPage/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainNibFile = ExtraPage; + INFOPLIST_KEY_NSPrincipalClass = InstallerSection; + INSTALL_PATH = "$(HOME)/Library/Bundles"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.website.my.ExtraPane; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; + ABACDD602C73F6BD002DA78F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ExtraPage/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainNibFile = ExtraPage; + INFOPLIST_KEY_NSPrincipalClass = InstallerSection; + INSTALL_PATH = "$(HOME)/Library/Bundles"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.website.my.ExtraPane; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + ABACDD452C73F6BD002DA78F /* Build configuration list for PBXProject "ExtraPage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ABACDD5C2C73F6BD002DA78F /* Debug */, + ABACDD5D2C73F6BD002DA78F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + ABACDD5E2C73F6BD002DA78F /* Build configuration list for PBXNativeTarget "ExtraPage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ABACDD5F2C73F6BD002DA78F /* Debug */, + ABACDD602C73F6BD002DA78F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = ABACDD422C73F6BD002DA78F /* Project object */; +} diff --git a/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/Base.lproj/ExtraPage.xib b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/Base.lproj/ExtraPage.xib new file mode 100644 index 000000000..4c0b759f6 --- /dev/null +++ b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/Base.lproj/ExtraPage.xib @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/ExtraPage.h b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/ExtraPage.h new file mode 100644 index 000000000..8e640f065 --- /dev/null +++ b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/ExtraPage.h @@ -0,0 +1,5 @@ +#import + +@interface ExtraPage : InstallerPane + +@end diff --git a/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/ExtraPage.m b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/ExtraPage.m new file mode 100644 index 000000000..7ccd91aff --- /dev/null +++ b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/ExtraPage.m @@ -0,0 +1,10 @@ +#import "ExtraPage.h" + +@implementation ExtraPage + +- (NSString *)title +{ + return [[NSBundle bundleForClass:[self class]] localizedStringForKey:@"Extra Page" value:nil table:nil]; +} + +@end diff --git a/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/Info.plist b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/Info.plist new file mode 100644 index 000000000..7ab3ae791 --- /dev/null +++ b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage/Info.plist @@ -0,0 +1,8 @@ + + + + + InstallerSectionTitle + Extra Page + + diff --git a/tests/test_examples.py b/tests/test_examples.py index 7a0975799..5554d6709 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -10,6 +10,7 @@ from datetime import timedelta from functools import lru_cache from pathlib import Path +from plistlib import load as plist_load from typing import Generator, Iterable, Optional, Tuple import pytest @@ -464,6 +465,37 @@ def test_example_osxpkg(tmp_path, request): assert expected == found +@pytest.mark.skipif(sys.platform != "darwin", reason="macOS only") +@pytest.mark.skipif(not shutil.which("xcodebuild"), reason="requires xcodebuild") +def test_example_osxpkg_extra_pages(tmp_path): + recipe_path = _example_path("osxpkg_extra_pages") + input_path = tmp_path / "input" + output_path = tmp_path / "output" + shutil.copytree(str(recipe_path), str(input_path)) + installer, install_dir = next(create_installer(input_path, output_path)) + # expand-full is an undocumented option that extracts all archives, + # including binary archives like the PlugIns file + cmd = ["pkgutil", "--expand-full", installer, output_path / "expanded"] + _execute(cmd) + installer_sections = output_path / "expanded" / "PlugIns" / "InstallerSections.plist" + assert installer_sections.exists() + + with open(installer_sections, "rb") as f: + plist = plist_load(f) + expected = { + "SectionOrder": [ + "Introduction", + "ReadMe", + "License", + "Target", + "PackageSelection", + "Install", + "ExtraPage.bundle", + ] + } + assert plist == expected + + def test_example_scripts(tmp_path, request): input_path = _example_path("scripts") for installer, install_dir in create_installer(input_path, tmp_path, with_spaces=True): From 59a02c62b25265e12fedecc16b0bc01a6e34e8bf Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 4 Sep 2024 14:42:00 -0700 Subject: [PATCH 03/35] Restructure signing --- constructor/osxpkg.py | 57 +++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index 36309a387..c12d8bbb2 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -432,14 +432,6 @@ def create_plugins(plugins_dir: str, info: dict): xcodebuild = shutil.which("xcodebuild") - if notarization_identity_name := info.get('notarization_identity_name'): - 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 page in pages: xcodeproj_dirs = [ file.resolve() @@ -461,37 +453,34 @@ def create_plugins(plugins_dir: str, info: dict): # do not create dSYM debug symbols directory "DEBUG_INFORMATION_FORMAT=", ] - if notarization_identity_name: - extra_flags = ( - "--verbose" - "--prefix {info.get('reverse_domain_identifier', info['name'])}" - "--options runtime" - ) - build_cmd.extend([ - f"CODE_SIGN_IDENTITY={notarization_identity_name}", - f"CODE_SIGN_ENTITLEMENTS={entitlements.name}", - f"OTHER_CODE_SIGN_FLAGS=\"{' '.join(extra_flags)}\"", - ]) explained_check_call(build_cmd) else: plugin_name = os.path.basename(page) page_in_plugins = join(plugins_dir, plugin_name) shutil.copytree(page, page_in_plugins) - if notarization_identity_name: - 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", entitlements.name, - page_in_plugins, - ] - ) - if notarization_identity_name: + + if notarization_identity_name := info.get('notarization_identity_name'): + 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(): + 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", entitlements.name, + str(path), + ] + ) os.unlink(entitlements.name) plugins = [file.name for file in Path(plugins_dir).iterdir()] From f35df22d6d6d2343010d9cf0007aa9c74dd9208d Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 4 Sep 2024 14:44:13 -0700 Subject: [PATCH 04/35] Use manual code signing for example --- .../plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj index 8cb946dd6..466b1d950 100644 --- a/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj +++ b/examples/osxpkg_extra_pages/plugins/ExtraPage/ExtraPage.xcodeproj/project.pbxproj @@ -277,9 +277,10 @@ ABACDD5F2C73F6BD002DA78F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ExtraPage/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -289,6 +290,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.website.my.ExtraPane; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; WRAPPER_EXTENSION = bundle; }; @@ -297,9 +299,10 @@ ABACDD602C73F6BD002DA78F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ExtraPage/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -309,6 +312,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.website.my.ExtraPane; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; WRAPPER_EXTENSION = bundle; }; From 44eac8f210b64932ae0e2c84b17abb374e778134 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 4 Sep 2024 15:54:44 -0700 Subject: [PATCH 05/35] Create CodeSign class --- constructor/osxpkg.py | 69 ++++++++++++++---------------------------- constructor/signing.py | 53 +++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 47 deletions(-) diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index c12d8bbb2..4abf9eaec 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -13,6 +13,7 @@ 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, @@ -423,13 +424,13 @@ def pkgbuild_prepare_installation(info): shutil.rmtree(f"{pkg}.expanded") -def create_plugins(plugins_dir: str, info: dict): - pages = info.get("post_install_pages") +def create_plugins(pages: list = None, codesigner: CodeSign = None): if not pages: return elif isinstance(pages, str): pages = [pages] + fresh_dir(PLUGINS_DIR) xcodebuild = shutil.which("xcodebuild") for page in pages: @@ -449,17 +450,17 @@ def create_plugins(plugins_dir: str, info: dict): xcodebuild, "-project", str(xcodeproj), - f"CONFIGURATION_BUILD_DIR={plugins_dir}", + f"CONFIGURATION_BUILD_DIR={PLUGINS_DIR}", # do not create dSYM debug symbols directory "DEBUG_INFORMATION_FORMAT=", ] explained_check_call(build_cmd) else: plugin_name = os.path.basename(page) - page_in_plugins = join(plugins_dir, plugin_name) + page_in_plugins = join(PLUGINS_DIR, plugin_name) shutil.copytree(page, page_in_plugins) - if notarization_identity_name := info.get('notarization_identity_name'): + if codesigner: with NamedTemporaryFile(suffix=".plist", delete=False) as entitlements: plist = { "com.apple.security.cs.allow-unsigned-executable-memory": True, @@ -467,24 +468,12 @@ def create_plugins(plugins_dir: str, info: dict): } plist_dump(plist, entitlements) - for path in Path(plugins_dir).iterdir(): - 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", entitlements.name, - str(path), - ] - ) + 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: + plugins = [file.name for file in Path(PLUGINS_DIR).iterdir()] + with open(join(PLUGINS_DIR, "InstallerSections.plist"), "wb") as f: plist = { "SectionOrder": [ "Introduction", @@ -522,12 +511,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()) @@ -575,31 +565,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! @@ -656,9 +635,7 @@ def create(info, verbose=False): modify_xml(xml_path, info) if plugins := info.get("post_install_pages"): - plugins_dir = join(CACHE_DIR, "plugins") - fresh_dir(plugins_dir) - create_plugins(plugins_dir, info) + create_plugins(plugins, codesigner=codesigner) identity_name = info.get('signing_identity_name') build_cmd = [ @@ -668,7 +645,7 @@ def create(info, verbose=False): "--identifier", info.get("reverse_domain_identifier", info['name']), ] if plugins: - build_cmd.extend(["--plugins", plugins_dir]) + 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: diff --git a/constructor/signing.py b/constructor/signing.py index 31e4e488f..b939f9615 100644 --- a/constructor/signing.py +++ b/constructor/signing.py @@ -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__) @@ -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) From 555eccb19ab4c3ed8cf5384bf66348b6d9613fd1 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 5 Sep 2024 15:24:35 -0700 Subject: [PATCH 06/35] Add testing for signed installers --- constructor/osxpkg.py | 20 +++-- .../create_self_signed_certificates_macos.sh | 82 +++++++++++++++++++ tests/conftest.py | 64 +++++++++++++++ tests/test_examples.py | 76 ++++++++++++++++- 4 files changed, 235 insertions(+), 7 deletions(-) create mode 100755 scripts/create_self_signed_certificates_macos.sh create mode 100644 tests/conftest.py diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index 4abf9eaec..56ee82d39 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -2,6 +2,7 @@ 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 @@ -431,7 +432,7 @@ def create_plugins(pages: list = None, codesigner: CodeSign = None): pages = [pages] fresh_dir(PLUGINS_DIR) - xcodebuild = shutil.which("xcodebuild") + xcodebuild = None for page in pages: xcodeproj_dirs = [ @@ -441,10 +442,19 @@ def create_plugins(pages: list = None, codesigner: CodeSign = None): ] if xcodeproj_dirs: if not xcodebuild: - raise RuntimeError( - "Plugin directory contains an uncompiled project," - " but xcodebuild is not available." - ) + 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 uncomplied project," + " but xcodebuild requires XCode to compile plugins." + ) for xcodeproj in xcodeproj_dirs: build_cmd = [ xcodebuild, diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh new file mode 100755 index 000000000..bb3dc3e23 --- /dev/null +++ b/scripts/create_self_signed_certificates_macos.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set +e + +if [[ -z "${ROOT_DIR}" ]]; then + ROOT_DIR=$(mktemp -d) +else + mkdir -p "${ROOT_DIR}" +fi + +# Array assignment may leave the first element empty, so run cut twice +openssl_lib=$(openssl version | cut -d' ' -f1) +openssl_version=$(openssl version | cut -d' ' -f2) +if [[ "${openssl_lib}" == "OpenSSL" ]] && [[ "${openssl_version}" == 3.* ]]; then + legacy=-legacy +fi + +APPLICATION_ROOT="application" +APPLICATION_SIGNING_ID=${APPLICATION_SIGNING_ID:-${APPLICATION_ROOT}} +INSTALLER_ROOT="installer" +INSTALLER_SIGNING_ID=${INSTALLER_SIGNING_ID:-${INSTALLER_ROOT}} +KEYCHAIN_PATH="${ROOT_DIR}/constructor.keychain-db" + +security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" +security set-keychain-settings -lut 3600 "${KEYCHAIN_PATH}" +security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" + +for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do + if [[ "${context}" == "${APPLICATION_ROOT}" ]]; then + keyusage="codeSigning" + certtype="1.2.840.113635.100.6.1.13" + commonname="${APPLICATION_SIGNING_ID}" + password="${APPLICATION_SIGNING_PASSWORD}" + else + keyusage="1.2.840.113635.100.4.13" + certtype="1.2.840.113635.100.6.1.14" + commonname="${INSTALLER_SIGNING_ID}" + password="${INSTALLER_SIGNING_PASSWORD}" + fi + + keyfile="${ROOT_DIR}/${context}.key" + p12file="${ROOT_DIR}/${context}.p12" + crtfile="${ROOT_DIR}/${context}.crt" + pemfile="${ROOT_DIR}/${INSTALLER_ROOT}.pem" + + openssl genrsa -out "${keyfile}" 2048 + openssl req -x509 -new -key "${keyfile}"\ + -out "${crtfile}"\ + -sha256\ + -days 1\ + -subj "/C=XX/ST=State/L=City/O=Company/OU=Org/CN=${commonname}/emailAddress=somebody@somewhere.com"\ + -addext "basicConstraints=critical,CA:FALSE"\ + -addext "extendedKeyUsage=critical,${keyusage}"\ + -addext "keyUsage=critical,digitalSignature"\ + -addext "${certtype}=critical,DER:0500" + + # shellcheck disable=SC2086 + openssl pkcs12 -export\ + -out "${p12file}"\ + -inkey "${keyfile}"\ + -in "${crtfile}"\ + -passout pass:"${password}"\ + ${legacy} + + security import "${p12file}" -P "${password}" -t cert -f pkcs12 -k "${KEYCHAIN_PATH}" -A + # shellcheck disable=SC2086 + openssl pkcs12 -in "${p12file}" -clcerts -nokeys -out "${pemfile}" ${legacy} -password pass:"${password}" + + # Output to verify installer signatures + fingerprint=$(openssl x509 -in "${pemfile}" -noout -fingerprint -sha256 | cut -f2 -d'=' | sed 's/://g') + echo "SHA256 ${commonname} = ${fingerprint}" + if [[ "${context}" == "installer" ]]; then + # Installer certificates must be trusted to be found in the keychain. + # In non-CI environments, users will be asked for a passkey. + security add-trusted-cert -p basic -k "${KEYCHAIN_PATH}" "${pemfile}" + fi +done + +# Add keychain at the beginning of the keychain list +# Must be removed at a later clean-up step +# shellcheck disable=SC2046 +security list-keychains -d user -s "${KEYCHAIN_PATH}" $(security list-keychains | xargs) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..43626cc1f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,64 @@ +import subprocess +from pathlib import Path + +import pytest + +REPO_DIR = Path(__file__).parent.parent + + +@pytest.fixture +def self_signed_certificate_macos(tmp_path): + p = subprocess.run( + ["security", "list-keychains", "-d", "user"], + capture_output=True, + text=True, + ) + current_keychains = [keychain.strip(' "') for keychain in p.stdout.split("\n") if keychain] + cert_root = tmp_path / "certs" + cert_root.mkdir(parents=True, exist_ok=True) + signing_identity = "testinstaller" + signing_identity_password = "1234" + notarization_identity = "testapplication" + notarization_identity_password = "5678" + keychain_password = "abcd" + env = { + "APPLICATION_SIGNING_ID": notarization_identity, + "APPLICATION_SIGNING_PASSWORD": notarization_identity_password, + "INSTALLER_SIGNING_ID": signing_identity, + "INSTALLER_SIGNING_PASSWORD": signing_identity_password, + "KEYCHAIN_PASSWORD": keychain_password, + "ROOT_DIR": str(cert_root), + } + p = subprocess.run( + ["bash", REPO_DIR / "scripts" / "create_self_signed_certificates_macos.sh"], + env=env, + capture_output=True, + text=True, + check=True, + ) + cert_data = { + "signing_identity": { + "name": signing_identity, + "sha256": "", + }, + "notarization_identity": { + "name": notarization_identity, + "sha256": "", + }, + } + for line in p.stdout.split("\n"): + if not line.startswith("SHA256"): + continue + identifier, sha256 = line.rsplit("=", 1) + if signing_identity in identifier: + cert_data["signing_identity"]["sha256"] = sha256.strip() + elif notarization_identity in identifier: + cert_data["notarization_identity"]["sha256"] = sha256.strip() + yield cert_data + # Clean up + p = subprocess.run( + ["security", "list-keychains", "-d", "user"], + capture_output=True, + text=True, + ) + subprocess.run(["security", "list-keychains", "-d", "user", "-s", *current_keychains]) diff --git a/tests/test_examples.py b/tests/test_examples.py index 5554d6709..daad25ae2 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -335,7 +335,7 @@ def _sort_by_extension(path): @lru_cache(maxsize=None) -def _self_signed_certificate(path: str, password: str = None): +def _self_signed_certificate_windows(path: str, password: str = None): if not sys.platform.startswith("win"): return return _execute( @@ -468,6 +468,10 @@ def test_example_osxpkg(tmp_path, request): @pytest.mark.skipif(sys.platform != "darwin", reason="macOS only") @pytest.mark.skipif(not shutil.which("xcodebuild"), reason="requires xcodebuild") def test_example_osxpkg_extra_pages(tmp_path): + try: + subprocess.run(["xcodebuild", "--help"], check=True, capture_output=True) + except subprocess.CalledProcessError: + pytest.skip("xcodebuild requires XCode to compile extra pages.") recipe_path = _example_path("osxpkg_extra_pages") input_path = tmp_path / "input" output_path = tmp_path / "output" @@ -496,6 +500,74 @@ def test_example_osxpkg_extra_pages(tmp_path): assert plist == expected +@pytest.mark.skipif(sys.platform != "darwin", reason="macOS only") +@pytest.mark.skipif(not shutil.which("xcodebuild"), reason="requires xcodebuild") +@pytest.mark.skipif("TEST_MACOS_SIGNING" not in os.environ, reason="TEST_MACOS_SIGNING not set") +def test_macos_signing(tmp_path, self_signed_certificate_macos): + try: + subprocess.run(["xcodebuild", "--help"], check=True, capture_output=True) + except subprocess.CalledProcessError: + pytest.skip("xcodebuild requires XCode to compile extra pages.") + notarization_identity = self_signed_certificate_macos["notarization_identity"] + signing_identity = self_signed_certificate_macos["signing_identity"] + input_path = tmp_path / "input" + recipe_path = _example_path("osxpkg_extra_pages") + shutil.copytree(str(recipe_path), str(input_path)) + with open(input_path / "construct.yaml", "a") as f: + f.write(f"notarization_identity_name: {notarization_identity['name']}\n") + f.write(f"signing_identity_name: {signing_identity['name']}\n") + output_path = tmp_path / "output" + installer, install_dir = next(create_installer(input_path, output_path)) + + # Check installer signature + p = subprocess.run( + ["pkgutil", "--check-signature", installer], + check=True, + capture_output=True, + text=True, + ) + installer_sha256 = "" + lines = p.stdout.split("\n") + nlines = len(lines) + assert nlines > 4 + for i in range(nlines - 4): + line = lines[i].strip() + if signing_identity["name"] in line and "SHA256" in lines[i + 2]: + i += 3 + while i < nlines and line: + installer_sha256 += lines[i].replace(" ", "") + i += 1 + break + assert installer_sha256 == signing_identity["sha256"] + + # Check component signatures + expanded_path = output_path / "expanded" + # expand-full is an undocumented option that extracts all archives, + # including binary archives like the PlugIns file + cmd = ["pkgutil", "--expand-full", installer, expanded_path] + _execute(cmd) + components = [ + Path(expanded_path, "prepare_installation.pkg", "Payload", "osx-pkg-test", "_conda"), + Path(expanded_path, "Plugins", "ExtraPage.bundle"), + ] + validated_signatures = [] + for component in components: + p = subprocess.run( + ["/usr/bin/codesign", "--verify", str(component), "--verbose=4"], + check=True, + text=True, + capture_output=True, + ) + lines = p.stderr.split("\n")[:-1] + if ( + len(lines) == 2 + and lines[0] == f"{component}: valid on disk" + and lines[1] == f"{component}: satisfies its Designated Requirement" + ): + validated_signatures.append(component) + assert validated_signatures == components + + def test_example_scripts(tmp_path, request): input_path = _example_path("scripts") for installer, install_dir in create_installer(input_path, tmp_path, with_spaces=True): @@ -547,7 +619,7 @@ def test_example_signing(tmp_path, request): input_path = _example_path("signing") cert_path = tmp_path / "self-signed-cert.pfx" cert_pwd = "1234" - _self_signed_certificate(path=cert_path, password=cert_pwd) + _self_signed_certificate_windows(path=cert_path, password=cert_pwd) assert cert_path.exists() certificate_in_input_dir = input_path / "certificate.pfx" shutil.copy(str(cert_path), str(certificate_in_input_dir)) From 769a07a053b996c8172528bcffc1166142c4a797 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 5 Sep 2024 15:39:08 -0700 Subject: [PATCH 07/35] Update documentation --- CONSTRUCT.md | 10 ++++++++++ docs/source/construct-yaml.md | 10 ++++++++++ docs/source/howto.md | 10 +++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CONSTRUCT.md b/CONSTRUCT.md index 33be341f1..36cdfaf0e 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -777,6 +777,16 @@ 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
+_types:_ list, string
+ +Adds extra panels to the installers to be shown after installation. +For pkg installers, these can be compiled installer plug-ins or +a directories containing an Xcode project. In the latter case, +constructor will try and compile the project file using `xcodebuild`. + ### `conclusion_file` _required:_ no
diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index 33be341f1..36cdfaf0e 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -777,6 +777,16 @@ 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
+_types:_ list, string
+ +Adds extra panels to the installers to be shown after installation. +For pkg installers, these can be compiled installer plug-ins or +a directories containing an Xcode project. In the latter case, +constructor will try and compile the project file using `xcodebuild`. + ### `conclusion_file` _required:_ no
diff --git a/docs/source/howto.md b/docs/source/howto.md index a5a8e54d4..c6fca5056 100644 --- a/docs/source/howto.md +++ b/docs/source/howto.md @@ -93,9 +93,17 @@ If neither `AZURE_SIGNTOOL_KEY_VAULT_ACCESSTOKEN` nor `AZURE_SIGNTOOL_KEY_VAULT_ In the case of macOS, users might get warnings for PKGs if the installers are not signed _and_ notarized. However, once these two requirements are fulfilled, the warnings disappear instantly. `constructor` offers some configuration options to help you in this process: -You will need to provide two identity names. One for the PKG signature (via [`signing_identity_name`](construct-yaml.md#signing_identity_name)), and one to pass the notarization (via [`notarization_identity_name`](construct-yaml.md#notarization_identity_name)). These can be obtained in the [Apple Developer portal](https://developer.apple.com/account/). +You will need to provide two identity names: +* the installer certificate identity (via [`signing_identity_name`](construct-yaml.md#signing_identity_name)) to sign the pkg installer, +* the application certificate identity to pass the notarization (via [`notarization_identity_name`](construct-yaml.md#notarization_identity_name)); + this certificate is used to sign binaries and plugins inside the pkg installer. +These can be obtained in the [Apple Developer portal](https://developer.apple.com/account/). Once signed, you can notarize your PKG with Apple's `notarytool`. +:::{note} + +To sign a pkg installer, the keychain containing the identity names must be unlocked and in the keychain search list. + ## Create shortcuts On Windows, `conda` supports `menuinst 1.x` shortcuts. If a package provides a certain JSON file From 71fcd61c232830c361f9a5684f61d4f9c1343deb Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 5 Sep 2024 16:00:20 -0700 Subject: [PATCH 08/35] Enable MacOS signing testing on CI --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8352293d2..523ab79f9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -150,6 +150,7 @@ jobs: AZURE_SIGNTOOL_KEY_VAULT_URL: ${{ secrets.AZURE_SIGNTOOL_KEY_VAULT_URL }} CONSTRUCTOR_EXAMPLES_KEEP_ARTIFACTS: "${{ runner.temp }}/examples_artifacts" CONSTRUCTOR_SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" + TEST_MACOS_SIGNING: 1 run: | rm -rf coverage.json pytest -vv --cov=constructor --cov-branch tests/test_examples.py From 18664529ee4af16c4e1a6f2d96e1183a50ed8544 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 09:07:26 -0700 Subject: [PATCH 09/35] Expand post-install pages to Windows --- CONSTRUCT.md | 8 ++++--- constructor/construct.py | 8 ++++--- constructor/nsis/main.nsi.tmpl | 5 ++++ constructor/winexe.py | 29 ++++++++++++++++++++--- docs/source/construct-yaml.md | 8 ++++--- examples/exe_extra_pages/construct.yaml | 10 ++++++++ examples/exe_extra_pages/extra_page_1.nsi | 12 ++++++++++ examples/exe_extra_pages/extra_page_2.nsi | 12 ++++++++++ tests/test_examples.py | 7 ++++++ 9 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 examples/exe_extra_pages/construct.yaml create mode 100644 examples/exe_extra_pages/extra_page_1.nsi create mode 100644 examples/exe_extra_pages/extra_page_2.nsi diff --git a/CONSTRUCT.md b/CONSTRUCT.md index 36cdfaf0e..bbbc5ced9 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -783,10 +783,14 @@ _required:_ no
_types:_ list, string
Adds extra panels to the installers to be shown after installation. + For pkg installers, these can be compiled installer plug-ins or a 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
@@ -798,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` diff --git a/constructor/construct.py b/constructor/construct.py index 9dc9e033b..fc7ba1f9c 100644 --- a/constructor/construct.py +++ b/constructor/construct.py @@ -573,9 +573,13 @@ ('post_install_pages', False, (list, str), ''' Adds extra panels to the installers to be shown after installation. + For pkg installers, these can be compiled installer plug-ins or a 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, ''' @@ -585,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, ''' diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 96190cf6e..8c3ad742c 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -155,6 +155,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 diff --git a/constructor/winexe.py b/constructor/winexe.py index cacdca569..e1108874e 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -302,14 +302,28 @@ def make_nsi( # for the newlines business replace['CONCLUSION_TEXT'] = "\r\n".join(conclusion_lines[1:]) - for key in ['welcome_file', 'conclusion_file']: + for key in ['welcome_file', 'conclusion_file', 'post_install_pages']: value = info.get(key, "") - if value and not value.endswith(".nsi"): + if not value: + continue + if isinstance(value, str) and not value.endswith(".nsi"): logger.warning( - "On Windows, %s must be a .nsi file; %s will be ignored.", + "On Windows, %s must be an .nsi file; %s will be ignored.", key, value, ) + elif isinstance(value, list): + valid_values = [] + for val in value: + if val.endswith(".nsi"): + valid_values.append(val) + else: + logger.warning( + "On Windows, %s must be .nsi files; %s will be ignored.", + key, + val, + ) + info[key] = valid_values for key, value in replace.items(): if value.startswith('@'): @@ -332,6 +346,7 @@ def make_nsi( ppd["has_conda"] = info["_has_conda"] ppd["custom_welcome"] = info.get("welcome_file", "").endswith(".nsi") ppd["custom_conclusion"] = info.get("conclusion_file", "").endswith(".nsi") + ppd["post_install_pages"] = bool(info.get("post_install_pages")) data = preprocess(data, ppd) data = fill_template(data, replace, exceptions=nsis_predefines) if info['_platform'].startswith("win") and sys.platform != 'win32': @@ -373,6 +388,12 @@ def make_nsi( if ppd['custom_conclusion'] else '' ), + ( + '@POST_INSTALL_PAGES@', + '\n'.join( + custom_nsi_insert_from_file(file) for file in info.get('post_install_pages', []) + ), + ), ('@TEMP_EXTRA_FILES@', '\n '.join(insert_tempfiles_commands(temp_extra_files))), ('@VIRTUAL_SPECS@', " ".join([f'"{spec}"' for spec in info.get("virtual_specs", ())])), @@ -382,6 +403,8 @@ def make_nsi( nsi_path = join(dir_path, 'main.nsi') with open(nsi_path, 'w') as fo: fo.write(data) + with open("main.nsi", "w") as fo: + fo.write(data) # Uncomment to see the file for debugging # with open('main.nsi', 'w') as fo: # fo.write(data) diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index 36cdfaf0e..bbbc5ced9 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -783,10 +783,14 @@ _required:_ no
_types:_ list, string
Adds extra panels to the installers to be shown after installation. + For pkg installers, these can be compiled installer plug-ins or a 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
@@ -798,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` diff --git a/examples/exe_extra_pages/construct.yaml b/examples/exe_extra_pages/construct.yaml new file mode 100644 index 000000000..95b649bf4 --- /dev/null +++ b/examples/exe_extra_pages/construct.yaml @@ -0,0 +1,10 @@ +name: extraPages +version: X +installer_type: all +channels: + - http://repo.anaconda.com/pkgs/main/ +specs: + - python +post_install_pages: + - extra_page_1.nsi + - extra_page_2.nsi diff --git a/examples/exe_extra_pages/extra_page_1.nsi b/examples/exe_extra_pages/extra_page_1.nsi new file mode 100644 index 000000000..c3c502d98 --- /dev/null +++ b/examples/exe_extra_pages/extra_page_1.nsi @@ -0,0 +1,12 @@ +Function extraPage1 +!insertmacro MUI_HEADER_TEXT "Extra Page 1" "This is extra page number 1" + +nsDialogs::Create 1018 +${NSD_CreateLabel} 0 0 100% 12u "Content of extra page 1" + +${NSD_CreateText} 0 13u 100% "Lorem ipsum dolor sit amet" + +nsDialogs::Show +FunctionEnd + +Page custom extraPage1 diff --git a/examples/exe_extra_pages/extra_page_2.nsi b/examples/exe_extra_pages/extra_page_2.nsi new file mode 100644 index 000000000..a59054222 --- /dev/null +++ b/examples/exe_extra_pages/extra_page_2.nsi @@ -0,0 +1,12 @@ +Function extraPage2 +!insertmacro MUI_HEADER_TEXT "Extra Page 2" "This is extra page number 2" + +nsDialogs::Create 1018 +${NSD_CreateLabel} 0 0 100% 12u "Content of extra page 2" + +${NSD_CreateText} 0 13u 100% "consectetur adipiscing elit." + +nsDialogs::Show +FunctionEnd + +Page custom extraPage2 diff --git a/tests/test_examples.py b/tests/test_examples.py index daad25ae2..9ddb53aaa 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -365,6 +365,13 @@ def test_example_customized_welcome_conclusion(tmp_path, request): _run_installer(input_path, installer, install_dir, request=request) +@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") +def test_example_extra_pages_win(tmp_path, request): + input_path = _example_path("exe_extra_pages") + for installer, install_dir in create_installer(input_path, tmp_path): + _run_installer(input_path, installer, install_dir, request=request) + + def test_example_extra_envs(tmp_path, request): input_path = _example_path("extra_envs") for installer, install_dir in create_installer(input_path, tmp_path): From c5971c14e4afada6b488e53167b2d63bb078d960 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 09:17:39 -0700 Subject: [PATCH 10/35] Clarify in tests that codesign outputs to stderr --- tests/test_examples.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_examples.py b/tests/test_examples.py index 9ddb53aaa..4b99f9c46 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -565,6 +565,7 @@ def test_macos_signing(tmp_path, self_signed_certificate_macos): text=True, capture_output=True, ) + # codesign --verify outputs to stderr lines = p.stderr.split("\n")[:-1] if ( len(lines) == 2 From 80479ac647f01a2dcebd5e0714d555b2bb0f4c36 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 09:20:42 -0700 Subject: [PATCH 11/35] Add news file --- news/852-pkg-extra-pages | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 news/852-pkg-extra-pages diff --git a/news/852-pkg-extra-pages b/news/852-pkg-extra-pages new file mode 100644 index 000000000..6cbcd512d --- /dev/null +++ b/news/852-pkg-extra-pages @@ -0,0 +1,19 @@ +### Enhancements + +* Add capability to add extra post-install pages to pkg installers. (#852) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 9b856c6eeb302652bb8b9c7d0368bd56e322f5b3 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 09:25:28 -0700 Subject: [PATCH 12/35] Remove debug code --- constructor/winexe.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/constructor/winexe.py b/constructor/winexe.py index e1108874e..99fe16c02 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -403,8 +403,6 @@ def make_nsi( nsi_path = join(dir_path, 'main.nsi') with open(nsi_path, 'w') as fo: fo.write(data) - with open("main.nsi", "w") as fo: - fo.write(data) # Uncomment to see the file for debugging # with open('main.nsi', 'w') as fo: # fo.write(data) From 9d080143d3c3d0955b4713337e26088cd1d2527c Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 10:23:32 -0700 Subject: [PATCH 13/35] Test: do not run add-trusted-cert on CI --- scripts/create_self_signed_certificates_macos.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh index bb3dc3e23..3417bb263 100755 --- a/scripts/create_self_signed_certificates_macos.sh +++ b/scripts/create_self_signed_certificates_macos.sh @@ -69,7 +69,7 @@ for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do # Output to verify installer signatures fingerprint=$(openssl x509 -in "${pemfile}" -noout -fingerprint -sha256 | cut -f2 -d'=' | sed 's/://g') echo "SHA256 ${commonname} = ${fingerprint}" - if [[ "${context}" == "installer" ]]; then + if [[ "${context}" == "installer" ]] && [[ -z "${CI}" ]]; then # Installer certificates must be trusted to be found in the keychain. # In non-CI environments, users will be asked for a passkey. security add-trusted-cert -p basic -k "${KEYCHAIN_PATH}" "${pemfile}" From ca9f855e6cebac0e0f81d99cf2f76567c721130d Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 10:44:24 -0700 Subject: [PATCH 14/35] Test: Skip trusting --- scripts/create_self_signed_certificates_macos.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh index 3417bb263..7b259f2e8 100755 --- a/scripts/create_self_signed_certificates_macos.sh +++ b/scripts/create_self_signed_certificates_macos.sh @@ -69,11 +69,11 @@ for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do # Output to verify installer signatures fingerprint=$(openssl x509 -in "${pemfile}" -noout -fingerprint -sha256 | cut -f2 -d'=' | sed 's/://g') echo "SHA256 ${commonname} = ${fingerprint}" - if [[ "${context}" == "installer" ]] && [[ -z "${CI}" ]]; then - # Installer certificates must be trusted to be found in the keychain. - # In non-CI environments, users will be asked for a passkey. - security add-trusted-cert -p basic -k "${KEYCHAIN_PATH}" "${pemfile}" - fi + #if [[ "${context}" == "installer" ]] && [[ -z "${CI}" ]]; then + # # Installer certificates must be trusted to be found in the keychain. + # # In non-CI environments, users will be asked for a passkey. + # security add-trusted-cert -p basic -k "${KEYCHAIN_PATH}" "${pemfile}" + #fi done # Add keychain at the beginning of the keychain list From 0a956b5915ce6be0e28449100030b0d854512d0a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 11:37:40 -0700 Subject: [PATCH 15/35] Add to system keychain --- scripts/create_self_signed_certificates_macos.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh index 7b259f2e8..a70536609 100755 --- a/scripts/create_self_signed_certificates_macos.sh +++ b/scripts/create_self_signed_certificates_macos.sh @@ -69,11 +69,12 @@ for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do # Output to verify installer signatures fingerprint=$(openssl x509 -in "${pemfile}" -noout -fingerprint -sha256 | cut -f2 -d'=' | sed 's/://g') echo "SHA256 ${commonname} = ${fingerprint}" - #if [[ "${context}" == "installer" ]] && [[ -z "${CI}" ]]; then - # # Installer certificates must be trusted to be found in the keychain. - # # In non-CI environments, users will be asked for a passkey. - # security add-trusted-cert -p basic -k "${KEYCHAIN_PATH}" "${pemfile}" - #fi + if [[ "${context}" == "installer" ]]; then + # Installer certificates must be trusted to be found in the keychain. + # In non-CI environments, users will be asked for a passkey. + security import "${pemfile}" -k /Library/Keychains/System.keychain -A + security add-trusted-cert -d -p basic -k /Library/Keychains/System.keychain "${pemfile}" + fi done # Add keychain at the beginning of the keychain list From 3f65457969d7b03e437603e74598ebdf00b6edc7 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 11:57:00 -0700 Subject: [PATCH 16/35] Use system keychain on CI --- .../create_self_signed_certificates_macos.sh | 20 +++++++++++++------ tests/conftest.py | 4 ++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh index a70536609..0896e9ed2 100755 --- a/scripts/create_self_signed_certificates_macos.sh +++ b/scripts/create_self_signed_certificates_macos.sh @@ -19,7 +19,18 @@ APPLICATION_ROOT="application" APPLICATION_SIGNING_ID=${APPLICATION_SIGNING_ID:-${APPLICATION_ROOT}} INSTALLER_ROOT="installer" INSTALLER_SIGNING_ID=${INSTALLER_SIGNING_ID:-${INSTALLER_ROOT}} -KEYCHAIN_PATH="${ROOT_DIR}/constructor.keychain-db" + +# Installer certificates must be trusted to be found in the keychain. +# Users will be asked for authentication. +# On GitHub runners, the system keychain does not require authentication, +# which is why it is unsed on the CI. +if [[ -n "${ON_CI}" ]]; then + KEYCHAIN_PATH="/Library/Keychains/System.keychain" + KEYCHAIN_DOMAIN="system" +else + KEYCHAIN_PATH="${ROOT_DIR}/constructor.keychain-db" + KEYCHAIN_DOMAIN="user" +fi security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" security set-keychain-settings -lut 3600 "${KEYCHAIN_PATH}" @@ -70,14 +81,11 @@ for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do fingerprint=$(openssl x509 -in "${pemfile}" -noout -fingerprint -sha256 | cut -f2 -d'=' | sed 's/://g') echo "SHA256 ${commonname} = ${fingerprint}" if [[ "${context}" == "installer" ]]; then - # Installer certificates must be trusted to be found in the keychain. - # In non-CI environments, users will be asked for a passkey. - security import "${pemfile}" -k /Library/Keychains/System.keychain -A - security add-trusted-cert -d -p basic -k /Library/Keychains/System.keychain "${pemfile}" + security add-trusted-cert -d -p basic -k "${KEYCHAIN_PATH}" "${pemfile}" fi done # Add keychain at the beginning of the keychain list # Must be removed at a later clean-up step # shellcheck disable=SC2046 -security list-keychains -d user -s "${KEYCHAIN_PATH}" $(security list-keychains | xargs) +security list-keychains -d ${KEYCHAIN_DOMAIN} -s "${KEYCHAIN_PATH}" $(security list-keychains | xargs) diff --git a/tests/conftest.py b/tests/conftest.py index 43626cc1f..65fcdbe21 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ +import os import subprocess from pathlib import Path import pytest REPO_DIR = Path(__file__).parent.parent +ON_CI = os.environ.get("CI") @pytest.fixture @@ -29,6 +31,8 @@ def self_signed_certificate_macos(tmp_path): "KEYCHAIN_PASSWORD": keychain_password, "ROOT_DIR": str(cert_root), } + if ON_CI: + env["ON_CI"] = "1" p = subprocess.run( ["bash", REPO_DIR / "scripts" / "create_self_signed_certificates_macos.sh"], env=env, From 5fc9d336916eab65a49f1c6a088fc2758379233e Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 12:51:30 -0700 Subject: [PATCH 17/35] Skip keychain domains --- scripts/create_self_signed_certificates_macos.sh | 4 +--- tests/conftest.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh index 0896e9ed2..c94a7a48f 100755 --- a/scripts/create_self_signed_certificates_macos.sh +++ b/scripts/create_self_signed_certificates_macos.sh @@ -26,10 +26,8 @@ INSTALLER_SIGNING_ID=${INSTALLER_SIGNING_ID:-${INSTALLER_ROOT}} # which is why it is unsed on the CI. if [[ -n "${ON_CI}" ]]; then KEYCHAIN_PATH="/Library/Keychains/System.keychain" - KEYCHAIN_DOMAIN="system" else KEYCHAIN_PATH="${ROOT_DIR}/constructor.keychain-db" - KEYCHAIN_DOMAIN="user" fi security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" @@ -88,4 +86,4 @@ done # Add keychain at the beginning of the keychain list # Must be removed at a later clean-up step # shellcheck disable=SC2046 -security list-keychains -d ${KEYCHAIN_DOMAIN} -s "${KEYCHAIN_PATH}" $(security list-keychains | xargs) +security list-keychains -s "${KEYCHAIN_PATH}" $(security list-keychains | xargs) diff --git a/tests/conftest.py b/tests/conftest.py index 65fcdbe21..214d761a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ @pytest.fixture def self_signed_certificate_macos(tmp_path): p = subprocess.run( - ["security", "list-keychains", "-d", "user"], + ["security", "list-keychains"], capture_output=True, text=True, ) @@ -65,4 +65,4 @@ def self_signed_certificate_macos(tmp_path): capture_output=True, text=True, ) - subprocess.run(["security", "list-keychains", "-d", "user", "-s", *current_keychains]) + subprocess.run(["security", "list-keychains", "-s", *current_keychains]) From 0dfc46c225ef7bd7a19716ecd362d8997fc88088 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 13:51:52 -0700 Subject: [PATCH 18/35] Set default keychain instead of using list-keychains --- .../create_self_signed_certificates_macos.sh | 21 ++++----------- tests/conftest.py | 26 ++++++++++++------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh index c94a7a48f..86466b060 100755 --- a/scripts/create_self_signed_certificates_macos.sh +++ b/scripts/create_self_signed_certificates_macos.sh @@ -20,18 +20,12 @@ APPLICATION_SIGNING_ID=${APPLICATION_SIGNING_ID:-${APPLICATION_ROOT}} INSTALLER_ROOT="installer" INSTALLER_SIGNING_ID=${INSTALLER_SIGNING_ID:-${INSTALLER_ROOT}} -# Installer certificates must be trusted to be found in the keychain. -# Users will be asked for authentication. -# On GitHub runners, the system keychain does not require authentication, -# which is why it is unsed on the CI. -if [[ -n "${ON_CI}" ]]; then - KEYCHAIN_PATH="/Library/Keychains/System.keychain" -else - KEYCHAIN_PATH="${ROOT_DIR}/constructor.keychain-db" -fi +KEYCHAIN_PATH="${KEYCHAIN_PATH:-"${ROOT_DIR}/constructor.keychain"}" -security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" -security set-keychain-settings -lut 3600 "${KEYCHAIN_PATH}" +if [[ ! -f "${KEYCHAIN_PATH}" ]]; then + security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" + security set-keychain-settings -lut 3600 "${KEYCHAIN_PATH}" +fi security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do @@ -82,8 +76,3 @@ for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do security add-trusted-cert -d -p basic -k "${KEYCHAIN_PATH}" "${pemfile}" fi done - -# Add keychain at the beginning of the keychain list -# Must be removed at a later clean-up step -# shellcheck disable=SC2046 -security list-keychains -s "${KEYCHAIN_PATH}" $(security list-keychains | xargs) diff --git a/tests/conftest.py b/tests/conftest.py index 214d761a8..9620c344b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,12 +10,12 @@ @pytest.fixture def self_signed_certificate_macos(tmp_path): - p = subprocess.run( - ["security", "list-keychains"], + default_keychain = subprocess.run( + ["security", "default-keychain"], capture_output=True, text=True, + check=True, ) - current_keychains = [keychain.strip(' "') for keychain in p.stdout.split("\n") if keychain] cert_root = tmp_path / "certs" cert_root.mkdir(parents=True, exist_ok=True) signing_identity = "testinstaller" @@ -31,8 +31,15 @@ def self_signed_certificate_macos(tmp_path): "KEYCHAIN_PASSWORD": keychain_password, "ROOT_DIR": str(cert_root), } + # Installer certificates must be trusted to be found in the keychain. + # Users will be asked for authentication. + # On GitHub runners, the system keychain does not require authentication, + # which is why it is unsed on the CI. if ON_CI: - env["ON_CI"] = "1" + keychain_path = "/Library/Keychains/System.keychain" + else: + keychain_path = str(cert_root / "constructor.keychain") + env["KEYCHAIN_PATH"] = keychain_path p = subprocess.run( ["bash", REPO_DIR / "scripts" / "create_self_signed_certificates_macos.sh"], env=env, @@ -58,11 +65,12 @@ def self_signed_certificate_macos(tmp_path): cert_data["signing_identity"]["sha256"] = sha256.strip() elif notarization_identity in identifier: cert_data["notarization_identity"]["sha256"] = sha256.strip() + subprocess.run yield cert_data # Clean up - p = subprocess.run( - ["security", "list-keychains", "-d", "user"], - capture_output=True, - text=True, + subprocess.run( + ["security", "default-keychain", "-s", default_keychain], + capture_output=True, + text=True, + check=True, ) - subprocess.run(["security", "list-keychains", "-s", *current_keychains]) From 7fc2d23063ce64c247b6a541a4554c8d1ef358f8 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 14:07:48 -0700 Subject: [PATCH 19/35] Actually set default keychain --- tests/conftest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9620c344b..ba9414e90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,12 +10,13 @@ @pytest.fixture def self_signed_certificate_macos(tmp_path): - default_keychain = subprocess.run( + p = subprocess.run( ["security", "default-keychain"], capture_output=True, text=True, check=True, ) + default_keychain = p.stdout.strip(' "\n') cert_root = tmp_path / "certs" cert_root.mkdir(parents=True, exist_ok=True) signing_identity = "testinstaller" @@ -65,7 +66,12 @@ def self_signed_certificate_macos(tmp_path): cert_data["signing_identity"]["sha256"] = sha256.strip() elif notarization_identity in identifier: cert_data["notarization_identity"]["sha256"] = sha256.strip() - subprocess.run + subprocess.run( + ["security", "default-keychain", "-s", keychain_path], + capture_output=True, + text=True, + check=True, + ) yield cert_data # Clean up subprocess.run( From 1ad573ded953413112b0e6ca23f8d596beff7fde Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 14:22:33 -0700 Subject: [PATCH 20/35] Unset password for system keychain --- tests/conftest.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ba9414e90..4f567c257 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,24 +23,25 @@ def self_signed_certificate_macos(tmp_path): signing_identity_password = "1234" notarization_identity = "testapplication" notarization_identity_password = "5678" - keychain_password = "abcd" - env = { - "APPLICATION_SIGNING_ID": notarization_identity, - "APPLICATION_SIGNING_PASSWORD": notarization_identity_password, - "INSTALLER_SIGNING_ID": signing_identity, - "INSTALLER_SIGNING_PASSWORD": signing_identity_password, - "KEYCHAIN_PASSWORD": keychain_password, - "ROOT_DIR": str(cert_root), - } # Installer certificates must be trusted to be found in the keychain. # Users will be asked for authentication. # On GitHub runners, the system keychain does not require authentication, # which is why it is unsed on the CI. if ON_CI: keychain_path = "/Library/Keychains/System.keychain" + keychain_password = "" else: keychain_path = str(cert_root / "constructor.keychain") - env["KEYCHAIN_PATH"] = keychain_path + keychain_password = "abcd" + env = { + "APPLICATION_SIGNING_ID": notarization_identity, + "APPLICATION_SIGNING_PASSWORD": notarization_identity_password, + "INSTALLER_SIGNING_ID": signing_identity, + "INSTALLER_SIGNING_PASSWORD": signing_identity_password, + "KEYCHAIN_PASSWORD": keychain_password, + "KEYCHAIN_PATH": keychain_path, + "ROOT_DIR": str(cert_root), + } p = subprocess.run( ["bash", REPO_DIR / "scripts" / "create_self_signed_certificates_macos.sh"], env=env, From 875af8947795ef19efe2dac52be8a7a5c48c53b0 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 15:00:03 -0700 Subject: [PATCH 21/35] Use system keychain only for certificate trusting --- scripts/create_self_signed_certificates_macos.sh | 13 ++++++++----- tests/conftest.py | 11 ++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh index 86466b060..ced60e529 100755 --- a/scripts/create_self_signed_certificates_macos.sh +++ b/scripts/create_self_signed_certificates_macos.sh @@ -21,11 +21,14 @@ INSTALLER_ROOT="installer" INSTALLER_SIGNING_ID=${INSTALLER_SIGNING_ID:-${INSTALLER_ROOT}} KEYCHAIN_PATH="${KEYCHAIN_PATH:-"${ROOT_DIR}/constructor.keychain"}" - -if [[ ! -f "${KEYCHAIN_PATH}" ]]; then - security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" - security set-keychain-settings -lut 3600 "${KEYCHAIN_PATH}" +if [[ -n "${ON_CI}" ]]; then + CERT_KEYCHAIN="/Library/Keychains/System.keychain" +else + CERT_KEYCHAIN=${KEYCHAIN_PATH} fi + +security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" +security set-keychain-settings -lut 3600 "${KEYCHAIN_PATH}" security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do @@ -73,6 +76,6 @@ for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do fingerprint=$(openssl x509 -in "${pemfile}" -noout -fingerprint -sha256 | cut -f2 -d'=' | sed 's/://g') echo "SHA256 ${commonname} = ${fingerprint}" if [[ "${context}" == "installer" ]]; then - security add-trusted-cert -d -p basic -k "${KEYCHAIN_PATH}" "${pemfile}" + security add-trusted-cert -d -p basic -k "${CERT_KEYCHAIN}" "${pemfile}" fi done diff --git a/tests/conftest.py b/tests/conftest.py index 4f567c257..e63cb24ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,21 +27,18 @@ def self_signed_certificate_macos(tmp_path): # Users will be asked for authentication. # On GitHub runners, the system keychain does not require authentication, # which is why it is unsed on the CI. - if ON_CI: - keychain_path = "/Library/Keychains/System.keychain" - keychain_password = "" - else: - keychain_path = str(cert_root / "constructor.keychain") - keychain_password = "abcd" + keychain_path = str(cert_root / "constructor.keychain") + keychain_password = "abcd" env = { "APPLICATION_SIGNING_ID": notarization_identity, "APPLICATION_SIGNING_PASSWORD": notarization_identity_password, "INSTALLER_SIGNING_ID": signing_identity, "INSTALLER_SIGNING_PASSWORD": signing_identity_password, "KEYCHAIN_PASSWORD": keychain_password, - "KEYCHAIN_PATH": keychain_path, "ROOT_DIR": str(cert_root), } + if not ON_CI: + env["ON_CI"] = "1" p = subprocess.run( ["bash", REPO_DIR / "scripts" / "create_self_signed_certificates_macos.sh"], env=env, From 5f5b2e4b06504a9dbfad053ed3bec8bd555e7744 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 15:13:01 -0700 Subject: [PATCH 22/35] Fix CI logic --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e63cb24ed..0b3b80d8e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,7 @@ def self_signed_certificate_macos(tmp_path): "KEYCHAIN_PASSWORD": keychain_password, "ROOT_DIR": str(cert_root), } - if not ON_CI: + if ON_CI: env["ON_CI"] = "1" p = subprocess.run( ["bash", REPO_DIR / "scripts" / "create_self_signed_certificates_macos.sh"], From 261fbe23deb820ae943a4dcbfeda0b71268187c1 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 16:45:02 -0700 Subject: [PATCH 23/35] Do not test for installer signing --- .../create_self_signed_certificates_macos.sh | 94 +++++++------------ tests/conftest.py | 53 +---------- tests/test_examples.py | 30 +----- 3 files changed, 44 insertions(+), 133 deletions(-) diff --git a/scripts/create_self_signed_certificates_macos.sh b/scripts/create_self_signed_certificates_macos.sh index ced60e529..02e2e067b 100755 --- a/scripts/create_self_signed_certificates_macos.sh +++ b/scripts/create_self_signed_certificates_macos.sh @@ -1,6 +1,6 @@ #!/bin/bash -set +e +set -e if [[ -z "${ROOT_DIR}" ]]; then ROOT_DIR=$(mktemp -d) @@ -15,67 +15,45 @@ if [[ "${openssl_lib}" == "OpenSSL" ]] && [[ "${openssl_version}" == 3.* ]]; the legacy=-legacy fi -APPLICATION_ROOT="application" APPLICATION_SIGNING_ID=${APPLICATION_SIGNING_ID:-${APPLICATION_ROOT}} -INSTALLER_ROOT="installer" -INSTALLER_SIGNING_ID=${INSTALLER_SIGNING_ID:-${INSTALLER_ROOT}} KEYCHAIN_PATH="${KEYCHAIN_PATH:-"${ROOT_DIR}/constructor.keychain"}" -if [[ -n "${ON_CI}" ]]; then - CERT_KEYCHAIN="/Library/Keychains/System.keychain" -else - CERT_KEYCHAIN=${KEYCHAIN_PATH} -fi - security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" security set-keychain-settings -lut 3600 "${KEYCHAIN_PATH}" security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" -for context in ${APPLICATION_ROOT} ${INSTALLER_ROOT}; do - if [[ "${context}" == "${APPLICATION_ROOT}" ]]; then - keyusage="codeSigning" - certtype="1.2.840.113635.100.6.1.13" - commonname="${APPLICATION_SIGNING_ID}" - password="${APPLICATION_SIGNING_PASSWORD}" - else - keyusage="1.2.840.113635.100.4.13" - certtype="1.2.840.113635.100.6.1.14" - commonname="${INSTALLER_SIGNING_ID}" - password="${INSTALLER_SIGNING_PASSWORD}" - fi - - keyfile="${ROOT_DIR}/${context}.key" - p12file="${ROOT_DIR}/${context}.p12" - crtfile="${ROOT_DIR}/${context}.crt" - pemfile="${ROOT_DIR}/${INSTALLER_ROOT}.pem" - - openssl genrsa -out "${keyfile}" 2048 - openssl req -x509 -new -key "${keyfile}"\ - -out "${crtfile}"\ - -sha256\ - -days 1\ - -subj "/C=XX/ST=State/L=City/O=Company/OU=Org/CN=${commonname}/emailAddress=somebody@somewhere.com"\ - -addext "basicConstraints=critical,CA:FALSE"\ - -addext "extendedKeyUsage=critical,${keyusage}"\ - -addext "keyUsage=critical,digitalSignature"\ - -addext "${certtype}=critical,DER:0500" - - # shellcheck disable=SC2086 - openssl pkcs12 -export\ - -out "${p12file}"\ - -inkey "${keyfile}"\ - -in "${crtfile}"\ - -passout pass:"${password}"\ - ${legacy} - - security import "${p12file}" -P "${password}" -t cert -f pkcs12 -k "${KEYCHAIN_PATH}" -A - # shellcheck disable=SC2086 - openssl pkcs12 -in "${p12file}" -clcerts -nokeys -out "${pemfile}" ${legacy} -password pass:"${password}" - - # Output to verify installer signatures - fingerprint=$(openssl x509 -in "${pemfile}" -noout -fingerprint -sha256 | cut -f2 -d'=' | sed 's/://g') - echo "SHA256 ${commonname} = ${fingerprint}" - if [[ "${context}" == "installer" ]]; then - security add-trusted-cert -d -p basic -k "${CERT_KEYCHAIN}" "${pemfile}" - fi -done +# Originally, this code contained code for creating certificates for installer signing: +# https://github.com/conda/constructor/blob/555eccb19ab4c3ed8cf5384bf66348b6d9613fd1/scripts/create_self_signed_certificates_macos.sh +# However, installer certificates must be trusted. Adding a trusted certificate to any +# keychain requires authentication, which is interactive and causes the run to hang. +APPLICATION_ROOT="application" +keyusage="codeSigning" +certtype="1.2.840.113635.100.6.1.13" +commonname="${APPLICATION_SIGNING_ID}" +password="${APPLICATION_SIGNING_PASSWORD}" +keyfile="${ROOT_DIR}/application.key" +p12file="${ROOT_DIR}/application.p12" +crtfile="${ROOT_DIR}/application.crt" + +openssl genrsa -out "${keyfile}" 2048 +openssl req -x509 -new -key "${keyfile}"\ + -out "${crtfile}"\ + -sha256\ + -days 1\ + -subj "/C=XX/ST=State/L=City/O=Company/OU=Org/CN=${commonname}/emailAddress=somebody@somewhere.com"\ + -addext "basicConstraints=critical,CA:FALSE"\ + -addext "extendedKeyUsage=critical,${keyusage}"\ + -addext "keyUsage=critical,digitalSignature"\ + -addext "${certtype}=critical,DER:0500" + +# shellcheck disable=SC2086 +openssl pkcs12 -export\ + -out "${p12file}"\ + -inkey "${keyfile}"\ + -in "${crtfile}"\ + -passout pass:"${password}"\ + ${legacy} + +security import "${p12file}" -P "${password}" -t cert -f pkcs12 -k "${KEYCHAIN_PATH}" -A +# shellcheck disable=SC2046 +security list-keychains -d user -s "${KEYCHAIN_PATH}" $(security list-keychains -d user | xargs) diff --git a/tests/conftest.py b/tests/conftest.py index 0b3b80d8e..e3cece0c0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,44 +1,30 @@ -import os import subprocess from pathlib import Path import pytest REPO_DIR = Path(__file__).parent.parent -ON_CI = os.environ.get("CI") @pytest.fixture -def self_signed_certificate_macos(tmp_path): +def self_signed_application_certificate_macos(tmp_path): p = subprocess.run( - ["security", "default-keychain"], + ["security", "list-keychains", "-d", "user"], capture_output=True, text=True, - check=True, ) - default_keychain = p.stdout.strip(' "\n') + current_keychains = [keychain.strip(' "') for keychain in p.stdout.split("\n") if keychain] cert_root = tmp_path / "certs" cert_root.mkdir(parents=True, exist_ok=True) - signing_identity = "testinstaller" - signing_identity_password = "1234" notarization_identity = "testapplication" notarization_identity_password = "5678" - # Installer certificates must be trusted to be found in the keychain. - # Users will be asked for authentication. - # On GitHub runners, the system keychain does not require authentication, - # which is why it is unsed on the CI. - keychain_path = str(cert_root / "constructor.keychain") keychain_password = "abcd" env = { "APPLICATION_SIGNING_ID": notarization_identity, "APPLICATION_SIGNING_PASSWORD": notarization_identity_password, - "INSTALLER_SIGNING_ID": signing_identity, - "INSTALLER_SIGNING_PASSWORD": signing_identity_password, "KEYCHAIN_PASSWORD": keychain_password, "ROOT_DIR": str(cert_root), } - if ON_CI: - env["ON_CI"] = "1" p = subprocess.run( ["bash", REPO_DIR / "scripts" / "create_self_signed_certificates_macos.sh"], env=env, @@ -46,35 +32,6 @@ def self_signed_certificate_macos(tmp_path): text=True, check=True, ) - cert_data = { - "signing_identity": { - "name": signing_identity, - "sha256": "", - }, - "notarization_identity": { - "name": notarization_identity, - "sha256": "", - }, - } - for line in p.stdout.split("\n"): - if not line.startswith("SHA256"): - continue - identifier, sha256 = line.rsplit("=", 1) - if signing_identity in identifier: - cert_data["signing_identity"]["sha256"] = sha256.strip() - elif notarization_identity in identifier: - cert_data["notarization_identity"]["sha256"] = sha256.strip() - subprocess.run( - ["security", "default-keychain", "-s", keychain_path], - capture_output=True, - text=True, - check=True, - ) - yield cert_data + yield notarization_identity # Clean up - subprocess.run( - ["security", "default-keychain", "-s", default_keychain], - capture_output=True, - text=True, - check=True, - ) + subprocess.run(["security", "list-keychains", "-d", "user", "-s", *current_keychains]) diff --git a/tests/test_examples.py b/tests/test_examples.py index 4b99f9c46..2cf57e124 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -509,44 +509,20 @@ def test_example_osxpkg_extra_pages(tmp_path): @pytest.mark.skipif(sys.platform != "darwin", reason="macOS only") @pytest.mark.skipif(not shutil.which("xcodebuild"), reason="requires xcodebuild") -@pytest.mark.skipif("TEST_MACOS_SIGNING" not in os.environ, reason="TEST_MACOS_SIGNING not set") -def test_macos_signing(tmp_path, self_signed_certificate_macos): +@pytest.mark.skipif("CI" not in os.environ, reason="CI only") +def test_macos_signing(tmp_path, self_signed_application_certificate_macos): try: subprocess.run(["xcodebuild", "--help"], check=True, capture_output=True) except subprocess.CalledProcessError: pytest.skip("xcodebuild requires XCode to compile extra pages.") - notarization_identity = self_signed_certificate_macos["notarization_identity"] - signing_identity = self_signed_certificate_macos["signing_identity"] input_path = tmp_path / "input" recipe_path = _example_path("osxpkg_extra_pages") shutil.copytree(str(recipe_path), str(input_path)) with open(input_path / "construct.yaml", "a") as f: - f.write(f"notarization_identity_name: {notarization_identity['name']}\n") - f.write(f"signing_identity_name: {signing_identity['name']}\n") + f.write(f"notarization_identity_name: {self_signed_application_certificate_macos}\n") output_path = tmp_path / "output" installer, install_dir = next(create_installer(input_path, output_path)) - # Check installer signature - p = subprocess.run( - ["pkgutil", "--check-signature", installer], - check=True, - capture_output=True, - text=True, - ) - installer_sha256 = "" - lines = p.stdout.split("\n") - nlines = len(lines) - assert nlines > 4 - for i in range(nlines - 4): - line = lines[i].strip() - if signing_identity["name"] in line and "SHA256" in lines[i + 2]: - i += 3 - while i < nlines and line: - installer_sha256 += lines[i].replace(" ", "") - i += 1 - break - assert installer_sha256 == signing_identity["sha256"] - # Check component signatures expanded_path = output_path / "expanded" # expand-full is an undocumented option that extracts all archives, From db7e7368a0aa2d2da0cb6efc9b87b097aa631343 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 6 Sep 2024 16:55:24 -0700 Subject: [PATCH 24/35] Remove special signing keyword --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 523ab79f9..8352293d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -150,7 +150,6 @@ jobs: AZURE_SIGNTOOL_KEY_VAULT_URL: ${{ secrets.AZURE_SIGNTOOL_KEY_VAULT_URL }} CONSTRUCTOR_EXAMPLES_KEEP_ARTIFACTS: "${{ runner.temp }}/examples_artifacts" CONSTRUCTOR_SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" - TEST_MACOS_SIGNING: 1 run: | rm -rf coverage.json pytest -vv --cov=constructor --cov-branch tests/test_examples.py From dd8e3478a2e11b8a49d3263055d090f111d53a20 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 9 Sep 2024 09:29:41 -0700 Subject: [PATCH 25/35] Set up debug session for macos-13 --- .github/workflows/main.yml | 84 +++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8352293d2..5b98c7dc1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,23 +39,23 @@ jobs: matrix: include: # UBUNTU - - os: ubuntu-latest - python-version: "3.8" - conda-standalone: conda-standalone - check-docs: true - - os: ubuntu-latest - python-version: "3.9" - conda-standalone: conda-standalone - check-docs: true - - os: ubuntu-latest - python-version: "3.10" - conda-standalone: conda-standalone-nightly - - os: ubuntu-latest - python-version: "3.11" - conda-standalone: micromamba - - os: ubuntu-latest - python-version: "3.12" - conda-standalone: conda-standalone + #- os: ubuntu-latest + # python-version: "3.8" + # conda-standalone: conda-standalone + # check-docs: true + #- os: ubuntu-latest + # python-version: "3.9" + # conda-standalone: conda-standalone + # check-docs: true + #- os: ubuntu-latest + # python-version: "3.10" + # conda-standalone: conda-standalone-nightly + #- os: ubuntu-latest + # python-version: "3.11" + # conda-standalone: micromamba + #- os: ubuntu-latest + # python-version: "3.12" + # conda-standalone: conda-standalone # MACOS - os: macos-13 python-version: "3.8" @@ -67,30 +67,30 @@ jobs: # - os: macos-13 # python-version: "3.10" # conda-standalone: micromamba - - os: macos-latest - python-version: "3.11" - conda-standalone: conda-standalone - - os: macos-latest - python-version: "3.12" - conda-standalone: micromamba - # WINDOWS - - os: windows-latest - python-version: "3.8" - conda-standalone: conda-standalone - - os: windows-latest - python-version: "3.9" - conda-standalone: conda-standalone-nightly - - os: windows-latest - python-version: "3.10" - conda-standalone: conda-standalone - - os: windows-latest - python-version: "3.11" - # conda-standalone: micromamba - conda-standalone: conda-standalone-nightly - - os: windows-latest - python-version: "3.12" - # conda-standalone: micromamba - conda-standalone: conda-standalone + #- os: macos-latest + # python-version: "3.11" + # conda-standalone: conda-standalone + #- os: macos-latest + # python-version: "3.12" + # conda-standalone: micromamba + ## WINDOWS + #- os: windows-latest + # python-version: "3.8" + # conda-standalone: conda-standalone + #- os: windows-latest + # python-version: "3.9" + # conda-standalone: conda-standalone-nightly + #- os: windows-latest + # python-version: "3.10" + # conda-standalone: conda-standalone + #- os: windows-latest + # python-version: "3.11" + # # conda-standalone: micromamba + # conda-standalone: conda-standalone-nightly + #- os: windows-latest + # python-version: "3.12" + # # conda-standalone: micromamba + # conda-standalone: conda-standalone env: PYTHONUNBUFFERED: "1" @@ -99,6 +99,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 - uses: conda-incubator/setup-miniconda@a4260408e20b96e80095f42ff7f1a15b27dd94ca # v3.0.4 with: activate-environment: constructor-dev From fcfee0fe8b9a7600cb2b1bd64ff104f52f4f1028 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 9 Sep 2024 09:31:45 -0700 Subject: [PATCH 26/35] Move tmate action behind example runs --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5b98c7dc1..818546287 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -99,8 +99,6 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - uses: conda-incubator/setup-miniconda@a4260408e20b96e80095f42ff7f1a15b27dd94ca # v3.0.4 with: activate-environment: constructor-dev @@ -157,6 +155,8 @@ jobs: pytest -vv --cov=constructor --cov-branch tests/test_examples.py coverage run --branch --append -m constructor -V coverage json + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} From 477ad90feb80690b829cf192a695cfeba66c5137 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 9 Sep 2024 09:32:56 -0700 Subject: [PATCH 27/35] Start session after setup steps --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 818546287..6686e1865 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -132,6 +132,8 @@ jobs: conda activate constructor-dev echo "CONSTRUCTOR_CONDA_EXE=$CONDA_PREFIX/standalone_conda/conda.exe" >> $GITHUB_ENV fi + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 - name: Run unit tests run: | pytest -vv --cov=constructor --cov-branch tests/ -m "not examples" @@ -155,8 +157,6 @@ jobs: pytest -vv --cov=constructor --cov-branch tests/test_examples.py coverage run --branch --append -m constructor -V coverage json - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} From 737d364a2cfd47ed893161addc4d9ff38715cccb Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 9 Sep 2024 09:49:20 -0700 Subject: [PATCH 28/35] Disable default shell --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6686e1865..8e8000aa4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,9 +27,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true -defaults: - run: - shell: bash -el {0} +#defaults: +# run: +# shell: bash -el {0} jobs: tests: name: ${{ matrix.os }}, Python ${{ matrix.python-version }}, ${{ matrix.conda-standalone }} From 5869348c7b4d7cc0098e98581850054aa31a36e5 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 9 Sep 2024 10:07:00 -0700 Subject: [PATCH 29/35] Do not set -e with default shell --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8e8000aa4..dfa106aa8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,9 +27,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true -#defaults: -# run: -# shell: bash -el {0} +defaults: + run: + shell: bash -l {0} jobs: tests: name: ${{ matrix.os }}, Python ${{ matrix.python-version }}, ${{ matrix.conda-standalone }} From 79aade50f57ebe160d772bbdc9548148ab37575e Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 9 Sep 2024 14:52:52 -0700 Subject: [PATCH 30/35] Copy full environment for self-signed certificates --- .github/workflows/main.yml | 86 +++++++++++++++++++------------------- tests/conftest.py | 6 ++- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dfa106aa8..8352293d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ concurrency: defaults: run: - shell: bash -l {0} + shell: bash -el {0} jobs: tests: name: ${{ matrix.os }}, Python ${{ matrix.python-version }}, ${{ matrix.conda-standalone }} @@ -39,23 +39,23 @@ jobs: matrix: include: # UBUNTU - #- os: ubuntu-latest - # python-version: "3.8" - # conda-standalone: conda-standalone - # check-docs: true - #- os: ubuntu-latest - # python-version: "3.9" - # conda-standalone: conda-standalone - # check-docs: true - #- os: ubuntu-latest - # python-version: "3.10" - # conda-standalone: conda-standalone-nightly - #- os: ubuntu-latest - # python-version: "3.11" - # conda-standalone: micromamba - #- os: ubuntu-latest - # python-version: "3.12" - # conda-standalone: conda-standalone + - os: ubuntu-latest + python-version: "3.8" + conda-standalone: conda-standalone + check-docs: true + - os: ubuntu-latest + python-version: "3.9" + conda-standalone: conda-standalone + check-docs: true + - os: ubuntu-latest + python-version: "3.10" + conda-standalone: conda-standalone-nightly + - os: ubuntu-latest + python-version: "3.11" + conda-standalone: micromamba + - os: ubuntu-latest + python-version: "3.12" + conda-standalone: conda-standalone # MACOS - os: macos-13 python-version: "3.8" @@ -67,30 +67,30 @@ jobs: # - os: macos-13 # python-version: "3.10" # conda-standalone: micromamba - #- os: macos-latest - # python-version: "3.11" - # conda-standalone: conda-standalone - #- os: macos-latest - # python-version: "3.12" - # conda-standalone: micromamba - ## WINDOWS - #- os: windows-latest - # python-version: "3.8" - # conda-standalone: conda-standalone - #- os: windows-latest - # python-version: "3.9" - # conda-standalone: conda-standalone-nightly - #- os: windows-latest - # python-version: "3.10" - # conda-standalone: conda-standalone - #- os: windows-latest - # python-version: "3.11" - # # conda-standalone: micromamba - # conda-standalone: conda-standalone-nightly - #- os: windows-latest - # python-version: "3.12" - # # conda-standalone: micromamba - # conda-standalone: conda-standalone + - os: macos-latest + python-version: "3.11" + conda-standalone: conda-standalone + - os: macos-latest + python-version: "3.12" + conda-standalone: micromamba + # WINDOWS + - os: windows-latest + python-version: "3.8" + conda-standalone: conda-standalone + - os: windows-latest + python-version: "3.9" + conda-standalone: conda-standalone-nightly + - os: windows-latest + python-version: "3.10" + conda-standalone: conda-standalone + - os: windows-latest + python-version: "3.11" + # conda-standalone: micromamba + conda-standalone: conda-standalone-nightly + - os: windows-latest + python-version: "3.12" + # conda-standalone: micromamba + conda-standalone: conda-standalone env: PYTHONUNBUFFERED: "1" @@ -132,8 +132,6 @@ jobs: conda activate constructor-dev echo "CONSTRUCTOR_CONDA_EXE=$CONDA_PREFIX/standalone_conda/conda.exe" >> $GITHUB_ENV fi - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - name: Run unit tests run: | pytest -vv --cov=constructor --cov-branch tests/ -m "not examples" diff --git a/tests/conftest.py b/tests/conftest.py index e3cece0c0..169c21a65 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import os import subprocess from pathlib import Path @@ -19,12 +20,13 @@ def self_signed_application_certificate_macos(tmp_path): notarization_identity = "testapplication" notarization_identity_password = "5678" keychain_password = "abcd" - env = { + env = os.environ.copy() + env.update({ "APPLICATION_SIGNING_ID": notarization_identity, "APPLICATION_SIGNING_PASSWORD": notarization_identity_password, "KEYCHAIN_PASSWORD": keychain_password, "ROOT_DIR": str(cert_root), - } + }) p = subprocess.run( ["bash", REPO_DIR / "scripts" / "create_self_signed_certificates_macos.sh"], env=env, From f11f2833c8f75a58c3184daa1cd49e393ddeb533 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 10 Sep 2024 07:05:06 -0700 Subject: [PATCH 31/35] Apply suggestions from code review Co-authored-by: jaimergp --- constructor/construct.py | 8 ++++---- constructor/osxpkg.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/constructor/construct.py b/constructor/construct.py index fc7ba1f9c..1a3a2b9e7 100644 --- a/constructor/construct.py +++ b/constructor/construct.py @@ -572,13 +572,13 @@ '''), ('post_install_pages', False, (list, str), ''' -Adds extra panels to the installers to be shown after installation. +Adds extra pages to the installers to be shown after installation. -For pkg installers, these can be compiled installer plug-ins or -a directories containing an Xcode project. In the latter case, +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. +For Windows, the extra pages must be `.nsi` files. They will be inserted as-is before the conclusion page. '''), diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index 56ee82d39..8df71e8b7 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -452,8 +452,8 @@ def create_plugins(pages: list = None, codesigner: CodeSign = None): subprocess.run([xcodebuild, "--help"], check=True, capture_output=True) except subprocess.CalledSubprocessError: raise RuntimeError( - "Plugin directory contains an uncomplied project," - " but xcodebuild requires XCode to compile plugins." + "Plugin directory contains an uncompiled project, " + "but xcodebuild requires XCode to compile plugins." ) for xcodeproj in xcodeproj_dirs: build_cmd = [ From 188c9b482ce6eb827ee6b63bc69bb11ca0d19ab2 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 10 Sep 2024 07:32:06 -0700 Subject: [PATCH 32/35] Update docs --- CONSTRUCT.md | 8 ++++---- docs/source/construct-yaml.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONSTRUCT.md b/CONSTRUCT.md index bbbc5ced9..92d93b572 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -782,13 +782,13 @@ You can disable it altogether if you set this key to `""` (empty string). _required:_ no
_types:_ list, string
-Adds extra panels to the installers to be shown after installation. +Adds extra pages to the installers to be shown after installation. -For pkg installers, these can be compiled installer plug-ins or -a directories containing an Xcode project. In the latter case, +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. +For Windows, the extra pages must be `.nsi` files. They will be inserted as-is before the conclusion page. ### `conclusion_file` diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index bbbc5ced9..92d93b572 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -782,13 +782,13 @@ You can disable it altogether if you set this key to `""` (empty string). _required:_ no
_types:_ list, string
-Adds extra panels to the installers to be shown after installation. +Adds extra pages to the installers to be shown after installation. -For pkg installers, these can be compiled installer plug-ins or -a directories containing an Xcode project. In the latter case, +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. +For Windows, the extra pages must be `.nsi` files. They will be inserted as-is before the conclusion page. ### `conclusion_file` From 1bda9715bdfedfd54f3235c2620b702621c2b567 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 10 Sep 2024 07:38:19 -0700 Subject: [PATCH 33/35] Factor out xcodebuild loop into function --- constructor/osxpkg.py | 51 ++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index 8df71e8b7..b2ae4c38a 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -426,13 +426,37 @@ def pkgbuild_prepare_installation(info): 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) - xcodebuild = None for page in pages: xcodeproj_dirs = [ @@ -441,30 +465,7 @@ def create_plugins(pages: list = None, codesigner: CodeSign = None): if file.suffix == ".xcodeproj" ] if xcodeproj_dirs: - if not xcodebuild: - 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) + _build_xcode_projects(xcodeproj_dirs) else: plugin_name = os.path.basename(page) page_in_plugins = join(PLUGINS_DIR, plugin_name) From 0c211da56bc96f993597d2e91892140d1b4a4f96 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 10 Sep 2024 08:04:36 -0700 Subject: [PATCH 34/35] Add plug-in examples --- examples/osxpkg_extra_pages/construct.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/osxpkg_extra_pages/construct.yaml b/examples/osxpkg_extra_pages/construct.yaml index 80eb29a82..36f47f00d 100644 --- a/examples/osxpkg_extra_pages/construct.yaml +++ b/examples/osxpkg_extra_pages/construct.yaml @@ -67,4 +67,7 @@ install_path_exists_error_text: > initialize_by_default: false register_python: False +# Examples for how to write these plugins: +# https://preserve.mactech.com/articles/mactech/Vol.25/25.06/InstallerPlugins/index.html +# http://s.sudre.free.fr/Stuff/Installer/Installer_Plugins/index.html post_install_pages: "plugins/ExtraPage" From c6858a1d69e20b9b4fc00650bfb8c5a739f340f3 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 10 Sep 2024 09:12:54 -0700 Subject: [PATCH 35/35] Fix typing for python 3.8 --- constructor/osxpkg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index b2ae4c38a..a2280adf2 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -9,6 +9,7 @@ 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 @@ -426,7 +427,7 @@ def pkgbuild_prepare_installation(info): def create_plugins(pages: list = None, codesigner: CodeSign = None): - def _build_xcode_projects(xcodeporj_dirs: list[Path]): + def _build_xcode_projects(xcodeporj_dirs: List[Path]): xcodebuild = shutil.which("xcodebuild") if not xcodebuild: raise RuntimeError(