mirror of
https://github.com/deskflow/deskflow.git
synced 2026-06-19 21:12:01 +08:00
* Restore Azure macOS dist scripts
* Move steps to workflow for testing
* Always upload to GitHub
* Add codesign ID
* Echo codesign ID
* Add cert import code
* Stub file for Mac
* Self-install pyyaml and choco
* Auto add env var on Windows
* Auto add CMAKE_PREFIX_PATH to .zshrc
* Shorter var names
* Append env var instead of replace
* Only set env var if not CI
* Improve function names and print output
* Simplify Linux package command
* Support continuation sequence
* Add note about Windows
* Remove dead doc file
* Tidy up version file and move to .env format
* Use Python venv for deps
* Only use venv on Mac
* Rename package script for all OS
* Add package and dist steps, and use common upload
* Remove version source
* Fixed vars not available
* Fixed python paths
* Use RuntimeError which is sufficient
* Remove dead code
* Add extras command for Linux
* Always install deps on Linux
* Move Python deps to CI
* More env bootstrapping, ugh
* Forgot to return!
* Simplify code
* Use shell
* Simplify command
* Skip sudo if no sudo
* Update package managers
* Fixed Fedora package name
* Tidy up commands
* Use newer upload artifact
* Strip don't trim!
* Check for version file and reduce log verbosity
* Remove CentOS 7.6
* Print more info about return code and log more to stderr
* Install certificate on macOS
* Better errors for no env var
* Implement Mac signing and notary
* Move dmgbuild load
* Simplify notary
* Rename dist files to same as dest
* Fixed paths for dist
* Move checked-in dist files to res (dist is meant to be a temp dir)
* Fixed Mac path in CMake
* Fixed dmg path
* Format Python
* Ignore import warnings and move function
* Fixed cmake paths
* Add missing env var secrets
* Remove extensions from GH upload
* Make deps.yml general purpose config
* Add cspell config
* Pass codesign ID
* Use new general config file
* Sign bundle on Mac
* Move imports to functions
* Escape chars in docs
* Fixed config key accessor
* Change module import order
* Move file to tmp dir in workflow dir
* Persist temp dir
* Add tmp dir to ignore
* Flush stdio before running process
* Trying quotes around env values
* Add codesigning certificate validation for Mac signing
* Revert "Trying quotes around env values"
This reverts commit 0dd741e8cd.
* Extract codesign verify
* Fixed version number
* Ignore .cache dir
* Fix macro name
* Package name with version number and arch
* Improve package function readability
* Change order of vars
* Testing upload to GDrive
* Add missing return code
* Use positional args and declare error
* Use machine instead of arch and remove build from filename
* Remove redundant build jobs
* Replace massively over-complicated `build_version.py` script
* Move version info to env module
* Use version info script
* Fixed: too many values to unpack
* Chmod version script
* Use shebang
* Don't check return code on Linux
* Fixed function name
* Convert to GitHub specific script
* Env vars must be after configure
* Fixed Windows env var command
* Remove && from deps command so it's not conditional
* Fixed position of set env
* Change order of env script
* Only upload when not draft
* Test
* Tweak config
* Fixed if condition
* Don't package in draft (Windows and Linux)
283 lines
8.8 KiB
Python
283 lines
8.8 KiB
Python
import os, subprocess, base64, time, json, shutil, sys
|
|
from lib import cmd_utils, env
|
|
|
|
cmake_env_var = "CMAKE_PREFIX_PATH"
|
|
shell_rc = "~/.zshrc"
|
|
cert_path = "tmp/codesign.p12"
|
|
dist_dir = "dist"
|
|
product_name = "Synergy"
|
|
settings_file = "res/dist/macos/dmgbuild/settings.py"
|
|
app_path = "build/bundle/Synergy.app"
|
|
security_path = "/usr/bin/security"
|
|
sudo_path = "/usr/bin/sudo"
|
|
notarytool_path = "/usr/bin/notarytool"
|
|
codesign_path = "/usr/bin/codesign"
|
|
xcode_select_path = "/usr/bin/xcode-select"
|
|
keychain_path = "/Library/Keychains/System.keychain"
|
|
|
|
|
|
def set_env_var(name, value):
|
|
text = f'export {name}="${name}:{value}"'
|
|
file = os.path.expanduser(shell_rc)
|
|
with open(file, "r") as f:
|
|
if text in f.read():
|
|
return
|
|
|
|
print(f"Setting environment variable: {name}={name}")
|
|
with open(file, "a") as f:
|
|
f.write(f"\n{text}")
|
|
print(f"Appended to {shell_rc}: {text}")
|
|
|
|
|
|
def set_cmake_prefix_env_var(cmake_prefix_command):
|
|
result = cmd_utils.run(cmake_prefix_command, get_output=True)
|
|
cmake_prefix = result.stdout.strip()
|
|
set_env_var(cmake_env_var, cmake_prefix)
|
|
|
|
|
|
def package(filename_base):
|
|
codesign_id = env.get_env_var("APPLE_CODESIGN_ID")
|
|
certificate = env.get_env_var("APPLE_P12_CERTIFICATE")
|
|
password = env.get_env_var("APPLE_P12_PASSWORD")
|
|
|
|
build_bundle()
|
|
install_certificate(certificate, password)
|
|
assert_certificate_installed(codesign_id)
|
|
sign_bundle(codesign_id)
|
|
dmg_path = build_dmg(filename_base)
|
|
notarize_package(dmg_path)
|
|
|
|
|
|
def build_bundle():
|
|
print("Building bundle...")
|
|
# cmake build install target should run macdeployqt
|
|
cmd_utils.run("cmake --build build --target install")
|
|
|
|
|
|
def sign_bundle(codesign_id):
|
|
print(f"Signing bundle {app_path}...")
|
|
sys.stdout.flush()
|
|
subprocess.run(
|
|
[
|
|
codesign_path,
|
|
"-f",
|
|
"--options",
|
|
"runtime",
|
|
"--deep",
|
|
"-s",
|
|
codesign_id,
|
|
app_path,
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
|
|
def assert_certificate_installed(codesign_id):
|
|
installed = cmd_utils.run(
|
|
"security find-identity -v -p codesigning", get_output=True
|
|
)
|
|
|
|
if codesign_id not in installed.stdout:
|
|
raise RuntimeError("Code signing certificate not installed or has expired")
|
|
|
|
|
|
def build_dmg(filename_base):
|
|
env.ensure_module("dmgbuild", "dmgbuild")
|
|
import dmgbuild # type: ignore
|
|
|
|
settings_file_abs = os.path.abspath(settings_file)
|
|
app_path_abs = os.path.abspath(app_path)
|
|
|
|
# cwd for dmgbuild, since setting the dmg filename to a path (include the dist dir) seems to
|
|
# make the dmg disappear and never writes to the specified path. the dmgbuild module also
|
|
# creates a temporary file in cwd, so it makes sense to change to the dist dir.
|
|
print(f"Changing directory to: {os.path.abspath(dist_dir)}")
|
|
cwd = os.getcwd()
|
|
os.makedirs(dist_dir, exist_ok=True)
|
|
os.chdir(dist_dir)
|
|
|
|
try:
|
|
dmg_filename = f"{filename_base}.dmg"
|
|
dmg_path = os.path.join(dist_dir, dmg_filename)
|
|
print(f"Building package {dmg_path}...")
|
|
dmgbuild.build_dmg(
|
|
dmg_filename,
|
|
product_name,
|
|
settings_file=settings_file_abs,
|
|
defines={
|
|
"app": app_path_abs,
|
|
},
|
|
)
|
|
finally:
|
|
print(f"Changing directory back to: {cwd}")
|
|
os.chdir(cwd)
|
|
|
|
return dmg_path
|
|
|
|
|
|
def install_certificate(cert_base64, cert_password):
|
|
if not cert_base64:
|
|
raise ValueError("Certificate base 64 not provided")
|
|
|
|
if not cert_password:
|
|
raise ValueError("Certificate password not provided")
|
|
|
|
print(f"Decoding certificate to: {cert_path}")
|
|
cert_bytes = base64.b64decode(cert_base64)
|
|
os.makedirs(os.path.dirname(cert_path), exist_ok=True)
|
|
with open(cert_path, "wb") as cert_file:
|
|
cert_file.write(cert_bytes)
|
|
|
|
print(f"Installing certificate: {cert_path}")
|
|
sys.stdout.flush()
|
|
|
|
try:
|
|
# warning: contains private key password, never print this command
|
|
subprocess.run(
|
|
[
|
|
sudo_path,
|
|
security_path,
|
|
"import",
|
|
cert_path,
|
|
"-k",
|
|
keychain_path,
|
|
"-P",
|
|
cert_password,
|
|
"-T",
|
|
codesign_path,
|
|
"-T",
|
|
security_path,
|
|
],
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
# important: suppress the original args with `from None` to avoid leaking the password
|
|
raise subprocess.CalledProcessError(e.returncode, security_path) from None
|
|
except Exception as e:
|
|
# important: suppress the original args with `from None` to avoid leaking the password
|
|
raise RuntimeError(f"Command failed: {security_path}") from None
|
|
finally:
|
|
# not strictly necessary for ci, but when run on a dev machine, it reduces the risk
|
|
# that private keys are left on the filesystem
|
|
print(f"Removing temporary certificate file: {cert_path}")
|
|
os.remove(cert_path)
|
|
|
|
|
|
def notarize_package(dmg_path):
|
|
print(f"Notarizing package {dmg_path}...")
|
|
notary_tool = NotaryTool()
|
|
notary_tool.store_credentials(
|
|
env.get_env_var("APPLE_NOTARY_USER"),
|
|
env.get_env_var("APPLE_NOTARY_PASSWORD"),
|
|
env.get_env_var("APPLE_TEAM_ID"),
|
|
)
|
|
|
|
notary_tool.submit_and_wait(dmg_path)
|
|
|
|
|
|
def get_xcode_path():
|
|
result = cmd_utils.run([xcode_select_path, "-p"], get_output=True, shell=False)
|
|
return result.stdout.strip()
|
|
|
|
|
|
class NotaryTool:
|
|
"""
|
|
Provides a wrapper around the notarytool command line tool.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.xcode_path = get_xcode_path()
|
|
|
|
def get_path(self):
|
|
return f"{self.xcode_path}{notarytool_path}"
|
|
|
|
def store_credentials(self, user, password, team_id):
|
|
print("Storing credentials for notary tool...")
|
|
sys.stdout.flush()
|
|
|
|
notarytool_path = self.get_path()
|
|
try:
|
|
# warning: contains password, never print this command
|
|
subprocess.run(
|
|
[
|
|
notarytool_path,
|
|
"store-credentials",
|
|
"notarytool-password",
|
|
"--apple-id",
|
|
user,
|
|
"--team-id",
|
|
team_id,
|
|
"--password",
|
|
password,
|
|
],
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
# important: suppress the original args with `from None` to avoid leaking the password
|
|
raise subprocess.CalledProcessError(e.returncode, notarytool_path) from None
|
|
except Exception as e:
|
|
# important: suppress the original args with `from None` to avoid leaking the password
|
|
raise RuntimeError(f"Command failed: {notarytool_path}") from None
|
|
|
|
def submit_and_wait(self, dmg_filename):
|
|
print("Submitting notarization request...")
|
|
submit_result = self.run_submit_command(dmg_filename)
|
|
request_id = submit_result["id"]
|
|
|
|
print(f"Notary submitted, waiting for request: {request_id}")
|
|
start = time.time()
|
|
wait_result = self.run_wait_command(request_id)
|
|
status = wait_result["status"]
|
|
|
|
time_taken = time.time() - start
|
|
print(f"Notary complete in {time_taken:.2f}s, status: {status}")
|
|
if status == "Accepted":
|
|
print("Notarization successful.")
|
|
elif status == "Invalid" or status == "Rejected":
|
|
raise ValueError(f"Notarization failed, status: {status}")
|
|
else:
|
|
raise ValueError(f"Unknown status: {status}")
|
|
|
|
def run_submit_command(self, dmg_filename):
|
|
if not os.path.exists(dmg_filename):
|
|
raise FileNotFoundError(f"File not found: {dmg_filename}")
|
|
|
|
result = cmd_utils.run(
|
|
[
|
|
self.get_path(),
|
|
"submit",
|
|
dmg_filename,
|
|
"--keychain-profile",
|
|
"notarytool-password",
|
|
"--output-format",
|
|
"json",
|
|
],
|
|
get_output=True,
|
|
shell=False,
|
|
)
|
|
|
|
if result.stderr:
|
|
return json.loads(result.stderr)
|
|
else:
|
|
return json.loads(result.stdout)
|
|
|
|
def run_wait_command(self, request_id):
|
|
result = cmd_utils.run(
|
|
[
|
|
self.get_path(),
|
|
"wait",
|
|
request_id,
|
|
"--keychain-profile",
|
|
"notarytool-password",
|
|
"--output-format",
|
|
"json",
|
|
],
|
|
get_output=True,
|
|
shell=False,
|
|
)
|
|
|
|
if result.stderr:
|
|
return json.loads(result.stderr)
|
|
else:
|
|
return json.loads(result.stdout)
|