OpenMiInk/third_party/ci-notary-service/signmacapp.py
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

142 lines
4.9 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-2-Clause
#
# SPDX-FileCopyrightText: 2023 KDE e.V.
# SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
# SPDX-FileCopyrightText: 2023 Julius Künzel <jk.kdedev@smartlab.uber.space>
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)