142 lines
4.9 KiB
Python
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)
|