Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 156 additions & 29 deletions build_scripts/apple_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
import sys
import locale
import os
import re
import platform
import shlex
import subprocess
from typing import Optional, List
from typing import Optional, List, Dict

TARGET_NATIVE = "native"
TARGET_X86 = "x86_64"
Expand Down Expand Up @@ -115,10 +116,8 @@ def GetTargetArchPair(context):
def SupportsMacOSUniversalBinaries():
if not MacOS():
return False
XcodeOutput = GetCommandOutput(["/usr/bin/xcodebuild", "-version"])
XcodeFind = XcodeOutput.rfind('Xcode ', 0, len(XcodeOutput))
XcodeVersion = XcodeOutput[XcodeFind:].split(' ')[1]
return (XcodeVersion > '11.0')
XcodeVersion = GetXcodeVersion()[0]
return (XcodeVersion > 11)

def GetSDKRoot(context) -> Optional[str]:
sdk = "macosx"
Expand Down Expand Up @@ -163,36 +162,164 @@ def ExtractFilesRecursive(path, cond):
files.append(os.path.join(r, file))
return files

def CodesignFiles(files):
SDKVersion = subprocess.check_output(
['xcodebuild', '-version']).strip()[6:10]
codeSignIDs = subprocess.check_output(
['security', 'find-identity', '-vp', 'codesigning'])

codeSignID = "-"
if os.environ.get('CODE_SIGN_ID'):
codeSignID = os.environ.get('CODE_SIGN_ID')
elif float(SDKVersion) >= 11.0 and \
codeSignIDs.find(b'Apple Development') != -1:
codeSignID = "Apple Development"
elif codeSignIDs.find(b'Mac Developer') != -1:
codeSignID = "Mac Developer"

for f in files:
subprocess.call(['codesign', '-f', '-s', '{codesignid}'
.format(codesignid=codeSignID), f],
stdout=devout, stderr=devout)

def Codesign(install_path, verbose_output=False):
def _GetCodeSignStringFromTerminal():
"""Return the output from the string codesigning variables"""
codeSignIDs = GetCommandOutput(['security', 'find-identity', '-vp', 'codesigning'])
return codeSignIDs


def GetXcodeVersion():
output = GetCommandOutput(['xcodebuild', '-version']).split()
version = float(output[1])
build = output[-1]

return version, build


def GetCodeSigningIdentifiers() -> Dict[str, str]:
"""Returns a dictionary of codesigning identifiers and their hashes"""
XcodeVersion = GetXcodeVersion()[0]
codeSignIDs = _GetCodeSignStringFromTerminal()

identifiers = {}
for codeSignID in (codeSignIDs or "").splitlines():
if "CSSMERR_TP_CERT_REVOKED" in codeSignID:
continue
if ")" not in codeSignID:
continue
if (XcodeVersion >= 11 and "Apple Development" in codeSignID) or "Mac Developer" in codeSignID:
identifier = codeSignID.split()[1]
identifier_hash = re.search(r'\(.*?\)', codeSignID)
if identifier_hash:
identifier_hash = identifier_hash[0][1:-1]
else:
identifier_hash = None

identifiers[identifier] = identifier_hash

identifiers["-"] = None
return identifiers


def GetCodeSignID() -> str:
"""Return the first code signing identifier"""
identifiers = GetCodeSigningIdentifiers()
env_signing_id = os.environ.get('CODE_SIGN_ID')
if env_signing_id:
if env_signing_id in identifiers:
return env_signing_id
raise RuntimeError(
f"Could not find environment specified identifier {env_signing_id} in registered code signing identifiers")

return list(GetCodeSigningIdentifiers().keys())[0]


def GetDevelopmentTeamID(identifier=None):
if "DEVELOPMENT_TEAM" in os.environ:
return os.environ.get("DEVELOPMENT_TEAM")

if not identifier:
identifier = GetCodeSignID()
if identifier == "-":
return None

identifier_hash = GetCodeSigningIdentifiers().get(identifier)
if not identifier_hash:
raise RuntimeError("Could not get identifiers hash")

certs = subprocess.check_output(["security", "find-certificate", "-c", identifier_hash, "-p"])
subject = GetCommandOutput(["openssl", "x509", "-subject"], input=certs)
subject = subject.splitlines()[0]
match = re.search("OU\s*=\s*(?P<team>([A-Za-z0-9_])+)", subject)
if not match:
raise RuntimeError("Could not parse the output certificate to find the team ID")

groups = match.groupdict()
team = groups.get("team")

if not team:
raise RuntimeError("Could not extract team id from certificate")

return team


def CodesignPath(path, identifier, team_identifier, force=False, is_framework=False) -> bool:
resign = force
if not force:
codesigning_info = GetCommandOutput(["codesign", "-vd", path])
if not codesigning_info:
resign = True
else:
# The output has multiple lines here
for line in codesigning_info.splitlines():
if line.startswith("TeamIdentifier="):
current_team_identifier = line.split("=")[-1]
if not current_team_identifier or "not set" in current_team_identifier:
resign = True
break
elif current_team_identifier == team_identifier:
break
else:
resign = True
Comment thread
dgovil marked this conversation as resolved.

if not resign:
return False

# Frameworks need to be signed with different parameters than loose binaries
if is_framework:
subprocess.check_call(
["codesign", "--force", "--sign", identifier, "--generate-entitlement-der", "--verbose", path])
else:
subprocess.check_call(["codesign", "--force", "--sign", identifier, path], stdout=devout, stderr=devout)
return True


def Codesign(install_path, identifier=None, force=False, verbose_output=False) -> bool:
if not MacOS():
return False

codeSignID = identifier or GetCodeSignID()

if verbose_output:
global devout
devout = sys.stdout
print(f"Code-signing files in {install_path} with {identifier}", file=devout)

files = ExtractFilesRecursive(install_path,
(lambda file: '.so' in file or '.dylib' in file))
CodesignFiles(files)
try:
team_identifier = GetDevelopmentTeamID(codeSignID)
except:
team_identifier = None

for root, dirs, files in os.walk(install_path, topdown=True):
for f in files:

_, ext = os.path.splitext(f)
if ext in (".dylib", ".so"):
path = os.path.join(root, f)
result = CodesignPath(path, identifier, team_identifier=team_identifier, force=force, is_framework=False)
if verbose_output:
if result:
print(f"Code-signed binary: {path}")
else:
print(f"Did not code-sign binary: {path}")

# Bit annoying to have to do this twice, but seems the fastest way to skip traversing frameworks
frameworks = [d for d in dirs if d.endswith(".framework")]
dirs[:] = [d for d in dirs if not d.endswith(".framework")]

for framework in frameworks:
framework_name = os.path.splitext(framework)[0]
if framework_name.lower() not in ["openusd", "opensubdiv", "materialx"]:
continue
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hardcoding a list of names, what do you think about just limiting the search to all .dylib, .so, and .framework files/directories in specific paths directory instead of walking the entire directory structure?

So something like:

codesignPaths = [
    os.path.join(install_dir, 'lib'),
    os.path.join(install_dir, 'plugin'),
    os.path.join(install_dir, 'share/usd')
]

for dir in codesignPaths:
    shared_libs = glob.glob(f"{os.path.join(dir, '*.dylib')")
    shared_libs += glob.glob(f"{os.path.join(dir, '*.so')")
    for lib in shared_libs:
        # Codesign..

    frameworks = glob.glob(f"{os.path.join(dir, '*.framework')")
    for framework in frameworks:
        # Codesign...

I like that this is more targeted -- the current implementation winds up signing files in unnecessary places like the build directory.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah that makes sense. I guess I hadn't considered that since I always install into a clean directory, but I'm assuming others may install into a communal directory?

Copy link
Copy Markdown
Collaborator Author

@dgovil dgovil Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. It's because the usdInstPath is not the final install directory but the full container directory.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunyab Okay, I pushed an update that integrates your solution to filter. I still use my walk within the filtered set because there are some dylibs that are within nested folders that need to be signed.
But it should hopefully resolve what you're seeing

path = os.path.join(root, framework)
result = CodesignPath(path, identifier, team_identifier=team_identifier, force=force, is_framework=True)
if verbose_output:
if result:
print(f"Code-signed framework: {path}")
else:
print(f"Did not code-sign framework: {path}")

return True

def CreateUniversalBinaries(context, libNames, x86Dir, armDir):
if not MacOS():
Expand Down
12 changes: 8 additions & 4 deletions build_scripts/build_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -2082,6 +2082,8 @@ def InstallUSD(context, force, buildArgs):
default=codesignDefault, action="store_true",
help=("Enable code signing for macOS builds "
"(defaults to enabled on Apple Silicon)"))
group.add_argument("--codesign-id", dest="macos_codesign_id", type=str,
help="A specific code-sign ID to use. If not provided, the build will try and find one or use '-'")

if Linux():
group.add_argument("--use-cxx11-abi", type=int, choices=[0, 1],
Expand Down Expand Up @@ -2392,9 +2394,9 @@ def __init__(self, args):
if MacOS():
apple_utils.SetTarget(self, self.buildTarget)

self.macOSCodesign = \
(args.macos_codesign if hasattr(args, "macos_codesign")
else False)
self.macOSCodesign = False
if args.macos_codesign:
self.macOSCodesign = args.macos_codesign_id or apple_utils.GetCodeSignID()
if apple_utils.IsHostArm() and args.ignore_homebrew:
self.ignorePaths.append("/opt/homebrew")
else:
Expand Down Expand Up @@ -2977,7 +2979,9 @@ def FormatBuildArguments(buildArgs):

if MacOS():
if context.macOSCodesign:
apple_utils.Codesign(context.usdInstDir, verbosity > 1)
apple_utils.Codesign(context.usdInstDir,
identifier=context.macOSCodesign,
verbose_output=verbosity > 1)

additionalInstructions = any([context.buildPython, context.buildTools, context.buildPrman])
if additionalInstructions:
Expand Down