#!/usr/bin/env python3 # SPDX-License-Identifier: BSD-2-Clause # # SPDX-FileCopyrightText: 2023 KDE e.V. # SPDX-FileContributor: Ingo Klöcker # SPDX-FileCopyrightText: 2023 Julius Künzel import logging import os import sys import tarfile from pathlib import Path from tempfile import TemporaryDirectory from typing import Optional from sftpnotary import config, projects, util from sftpnotary.exceptions import Error from sftpnotary.macappsigner import SignMacAppJob log = logging.getLogger("signmacapp") def parseCommandLine(): import argparse parser = argparse.ArgumentParser(description="Sign macOS apps with signing service") # debug options parser.add_argument("-v", "--verbose", action="count", default=0, help="increase the verbosity") parser.add_argument("--config") parser.add_argument("appfile", help="the .dmg file or .app (inside tar archive) to sign") options = parser.parse_args() return options def logTaskLog(logFile: Path): log.info("Logs of remote signing:") with open(logFile, "r", encoding="utf-8") as f: for line in f: log.info(line.rstrip()) def signMacApp(app: Path, logFolder: Path, projectPath: Optional[str], branch: Optional[str]) -> bool: # Look for project-specific entitlements file using CI_PROJECT_NAME # For .app signing, logFolder is the original .app parent directory where entitlements were copied entitlementsFile = None projectName = os.environ.get("CI_PROJECT_NAME") if projectName: entitlementsPath = logFolder / f"{projectName}.entitlements" if entitlementsPath.exists(): entitlementsFile = entitlementsPath log.info(f"Found entitlements file: {entitlementsFile.name} at {logFolder}") else: log.info(f"No entitlements file found at {entitlementsPath}") else: log.warning("CI_PROJECT_NAME not set, cannot look for entitlements file") with util.connectToSftpServer() as sftp: job = SignMacAppJob( sftp, macAppFilePath=app, entitlementsFilePath=entitlementsFile, logFolder=logFolder, token=os.environ.get("CI_JOB_TOKEN"), projectPath=projectPath, branch=branch, ) try: job.start() job.waitForCompletion() except Exception as e: # or more specific exception log.error("Error: SignMacAppJob failed", exc_info=e) for logFile in job.logFiles: if logFile.name == "task.log": logTaskLog(logFile) if not job.success: log.error("Error: Signing macOS app failed") return job.success def main() -> int: options = parseCommandLine() config.loadConfig(options.config) util.setUpClientLogging(log, options.verbose) projectSettingsLocation = config.settings.get("General", "ProjectSettings", "macappsigner-projects.yaml") util.loadProjectSettings( projectSettingsLocation, relativeTo=Path(options.config).absolute().parent, ) # Ignore merge request pipelines if os.environ.get("CI_PIPELINE_SOURCE") == "merge_request_event": log.info("Merge request pipelines are not cleared for signing. Skipping.") return 0 # Check if it makes sense to attempt signing the macOS file projectPath = os.environ["CI_PROJECT_PATH"] branch = os.environ["CI_COMMIT_REF_NAME"] if not projects.settings.exists(projectPath, branch): log.info( f"""Branch '{branch}' of project '{projectPath}' is not cleared for signing. Skipping.\n You can clear it by adding the branch to {projectSettingsLocation}""" ) return 0 appFilePath = Path(options.appfile) if appFilePath.suffix == ".app": # Build a tarball since .app files are basically special folders with TemporaryDirectory() as tempArchiveDir: appArchivePath = Path(tempArchiveDir) / "shuttleArchive" with tarfile.open(name=appArchivePath, mode="w") as archive: archive.add(appFilePath, arcname=appFilePath.name, recursive=True) success = signMacApp(appArchivePath, appFilePath.parent, projectPath, branch) if success: # signMacApp overwrites the archive with the unsigned content # so we can just extract the archive at the same location again util.extractTarArchive(appArchivePath, appFilePath.parent) else: success = signMacApp(appFilePath, appFilePath.parent, projectPath, branch) if not success: return 1 util.runCommand(["codesign", "--display", "--verbose", appFilePath], cwd=appFilePath.parent) util.runCommand(["codesign", "--verify", "--strict", "--deep", appFilePath], cwd=appFilePath.parent) return 0 if __name__ == "__main__": try: sys.exit(main()) except Error as e: print("Error: %s" % e, file=sys.stderr) sys.exit(1)