2023-04-26 14:57:43 +08:00
#! /usr/bin/python3
#
2019-08-17 13:47:43 +08:00
# File: SMJobBlessUtil.py
2023-04-26 14:57:43 +08:00
#
2019-08-17 13:47:43 +08:00
# Contains: Tool for checking and correcting apps that use SMJobBless.
2023-04-26 14:57:43 +08:00
#
2019-08-17 13:47:43 +08:00
# Written by: DTS
2023-04-26 14:57:43 +08:00
#
2019-08-17 13:47:43 +08:00
# Copyright: Copyright (c) 2012 Apple Inc. All Rights Reserved.
2023-04-26 14:57:43 +08:00
#
2019-08-17 13:47:43 +08:00
# Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc.
# ("Apple") in consideration of your agreement to the following
# terms, and your use, installation, modification or
# redistribution of this Apple software constitutes acceptance of
# these terms. If you do not agree with these terms, please do
# not use, install, modify or redistribute this Apple software.
2023-04-26 14:57:43 +08:00
#
2019-08-17 13:47:43 +08:00
# In consideration of your agreement to abide by the following
# terms, and subject to these terms, Apple grants you a personal,
# non-exclusive license, under Apple's copyrights in this
# original Apple software (the "Apple Software"), to use,
# reproduce, modify and redistribute the Apple Software, with or
# without modifications, in source and/or binary forms; provided
# that if you redistribute the Apple Software in its entirety and
# without modifications, you must retain this notice and the
# following text and disclaimers in all such redistributions of
# the Apple Software. Neither the name, trademarks, service marks
# or logos of Apple Inc. may be used to endorse or promote
# products derived from the Apple Software without specific prior
# written permission from Apple. Except as expressly stated in
# this notice, no other rights or licenses, express or implied,
# are granted by Apple herein, including but not limited to any
# patent rights that may be infringed by your derivative works or
# by other works in which the Apple Software may be incorporated.
2023-04-26 14:57:43 +08:00
#
# The Apple Software is provided by Apple on an "AS IS" basis.
2019-08-17 13:47:43 +08:00
# APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
# WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT,
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING
# THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
# COMBINATION WITH YOUR PRODUCTS.
2023-04-26 14:57:43 +08:00
#
2019-08-17 13:47:43 +08:00
# IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
# INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY
# OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
# OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY
# OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR
# OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
2023-04-26 14:57:43 +08:00
#
2019-08-17 13:47:43 +08:00
import sys
import os
import getopt
import subprocess
import plistlib
import operator
2023-04-26 14:57:43 +08:00
import platform
2019-08-17 13:47:43 +08:00
class UsageException ( Exception ) :
"""
2023-04-26 14:57:43 +08:00
Raised when the progam detects a usage issue ; the top - level code catches this
2019-08-17 13:47:43 +08:00
and prints a usage message .
"""
pass
class CheckException ( Exception ) :
"""
2023-04-26 14:57:43 +08:00
Raised when the " check " subcommand detects a problem ; the top - level code catches
2019-08-17 13:47:43 +08:00
this and prints a nice error message .
"""
def __init__ ( self , message , path = None ) :
self . message = message
self . path = path
def checkCodeSignature ( programPath , programType ) :
""" Checks the code signature of the referenced program. """
2023-04-26 14:57:43 +08:00
# Use the codesign tool to check the signature. The second "-v" is required to enable
# verbose mode, which causes codesign to do more checking. By default it does the minimum
# amount of checking ("Is the program properly signed?"). If you enabled verbose mode it
# does other sanity checks, which we definitely want. The specific thing I'd like to
# detect is "Does the code satisfy its own designated requirement?" and I need to enable
2019-08-17 13:47:43 +08:00
# verbose mode to get that.
args = [
2023-04-26 14:57:43 +08:00
# "false",
" codesign " ,
" -v " ,
2019-08-17 13:47:43 +08:00
" -v " ,
programPath
]
try :
subprocess . check_call ( args , stderr = open ( " /dev/null " ) )
2023-04-26 14:57:43 +08:00
except subprocess . CalledProcessError as e :
2019-08-17 13:47:43 +08:00
raise CheckException ( " %s code signature invalid " % programType , programPath )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
def readDesignatedRequirement ( programPath , programType ) :
""" Returns the designated requirement of the program as a string. """
args = [
2023-04-26 14:57:43 +08:00
# "false",
" codesign " ,
" -d " ,
" -r " ,
" - " ,
2019-08-17 13:47:43 +08:00
programPath
]
try :
2023-04-26 14:57:43 +08:00
req = subprocess . check_output ( args , stderr = open ( " /dev/null " ) , encoding = " utf-8 " )
except subprocess . CalledProcessError as e :
2019-08-17 13:47:43 +08:00
raise CheckException ( " %s designated requirement unreadable " % programType , programPath )
reqLines = req . splitlines ( )
if len ( reqLines ) != 1 or not req . startswith ( " designated => " ) :
raise CheckException ( " %s designated requirement malformed " % programType , programPath )
return reqLines [ 0 ] [ len ( " designated => " ) : ]
def readInfoPlistFromPath ( infoPath ) :
""" Reads an " Info.plist " file from the specified path. """
try :
2023-04-26 14:57:43 +08:00
with open ( infoPath , ' rb ' ) as fp :
info = plistlib . load ( fp )
2019-08-17 13:47:43 +08:00
except :
raise CheckException ( " ' Info.plist ' not readable " , infoPath )
if not isinstance ( info , dict ) :
raise CheckException ( " ' Info.plist ' root must be a dictionary " , infoPath )
return info
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
def readPlistFromToolSection ( toolPath , segmentName , sectionName ) :
""" Reads a dictionary property list from the specified section within the specified executable. """
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
# Run otool -s to get a hex dump of the section.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
args = [
2023-04-26 14:57:43 +08:00
# "false",
" otool " ,
" -V " ,
" -arch " ,
platform . machine ( ) ,
" -s " ,
segmentName ,
sectionName ,
2019-08-17 13:47:43 +08:00
toolPath
]
try :
2023-04-26 14:57:43 +08:00
plistDump = subprocess . check_output ( args , encoding = " utf-8 " )
except subprocess . CalledProcessError as e :
2019-08-17 13:47:43 +08:00
raise CheckException ( " tool %s / %s section unreadable " % ( segmentName , sectionName ) , toolPath )
2023-04-26 14:57:43 +08:00
# Convert that dump to an property list.
plistLines = plistDump . strip ( ) . splitlines ( keepends = True )
if len ( plistLines ) < 3 :
2019-08-17 13:47:43 +08:00
raise CheckException ( " tool %s / %s section dump malformed (1) " % ( segmentName , sectionName ) , toolPath )
2023-04-26 14:57:43 +08:00
header = plistLines [ 1 ] . strip ( )
if not header . endswith ( " ( %s , %s ) section " % ( segmentName , sectionName ) ) :
raise CheckException ( " tool %s / %s section dump malformed (2) " % ( segmentName , sectionName ) , toolPath )
2019-08-17 13:47:43 +08:00
del plistLines [ 0 : 2 ]
try :
2023-04-26 14:57:43 +08:00
if header . startswith ( ' Contents of ' ) :
data = [ ]
for line in plistLines :
# line looks like this:
#
# '100000000 3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d 22 31 |<?xml version="1|'
parts = line . split ( ' | ' )
assert len ( parts ) == 3
columns = parts [ 0 ] . split ( )
assert len ( columns ) > = 2
del columns [ 0 ]
for hexStr in columns :
data . append ( int ( hexStr , 16 ) )
data = bytes ( data )
else :
data = bytes ( " " . join ( plistLines ) , encoding = " utf-8 " )
plist = plistlib . loads ( data )
2019-08-17 13:47:43 +08:00
except :
2023-04-26 14:57:43 +08:00
raise CheckException ( " tool %s / %s section dump malformed (3) " % ( segmentName , sectionName ) , toolPath )
2019-08-17 13:47:43 +08:00
# Check the root of the property list.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
if not isinstance ( plist , dict ) :
raise CheckException ( " tool %s / %s property list root must be a dictionary " % ( segmentName , sectionName ) , toolPath )
return plist
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
def checkStep1 ( appPath ) :
""" Checks that the app and the tool are both correctly code signed. """
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
if not os . path . isdir ( appPath ) :
raise CheckException ( " app not found " , appPath )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
# Check the app's code signature.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
checkCodeSignature ( appPath , " app " )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
# Check the tool directory.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
toolDirPath = os . path . join ( appPath , " Contents " , " Library " , " LaunchServices " )
if not os . path . isdir ( toolDirPath ) :
raise CheckException ( " tool directory not found " , toolDirPath )
# Check each tool's code signature.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
toolPathList = [ ]
for toolName in os . listdir ( toolDirPath ) :
if toolName != " .DS_Store " :
toolPath = os . path . join ( toolDirPath , toolName )
if not os . path . isfile ( toolPath ) :
raise CheckException ( " tool directory contains a directory " , toolPath )
checkCodeSignature ( toolPath , " tool " )
toolPathList . append ( toolPath )
# Check that we have at least one tool.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
if len ( toolPathList ) == 0 :
raise CheckException ( " no tools found " , toolDirPath )
return toolPathList
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
def checkStep2 ( appPath , toolPathList ) :
""" Checks the SMPrivilegedExecutables entry in the app ' s " Info.plist " . """
# Create a map from the tool name (not path) to its designated requirement.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
toolNameToReqMap = dict ( )
for toolPath in toolPathList :
req = readDesignatedRequirement ( toolPath , " tool " )
toolNameToReqMap [ os . path . basename ( toolPath ) ] = req
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
# Read the Info.plist for the app and extract the SMPrivilegedExecutables value.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
infoPath = os . path . join ( appPath , " Contents " , " Info.plist " )
info = readInfoPlistFromPath ( infoPath )
2023-04-26 14:57:43 +08:00
if " SMPrivilegedExecutables " not in info :
2019-08-17 13:47:43 +08:00
raise CheckException ( " ' SMPrivilegedExecutables ' not found " , infoPath )
infoToolDict = info [ " SMPrivilegedExecutables " ]
if not isinstance ( infoToolDict , dict ) :
raise CheckException ( " ' SMPrivilegedExecutables ' must be a dictionary " , infoPath )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
# Check that the list of tools matches the list of SMPrivilegedExecutables entries.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
if sorted ( infoToolDict . keys ( ) ) != sorted ( toolNameToReqMap . keys ( ) ) :
raise CheckException ( " ' SMPrivilegedExecutables ' and tools in ' Contents/Library/LaunchServices ' don ' t match " )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
# Check that all the requirements match.
2023-04-26 14:57:43 +08:00
# This is an interesting policy choice. Technically the tool just needs to match
# the requirement listed in SMPrivilegedExecutables, and we can check that by
2019-08-17 13:47:43 +08:00
# putting the requirement into tmp.req and then running
#
# $ codesign -v -R tmp.req /path/to/tool
#
2023-04-26 14:57:43 +08:00
# However, for a Developer ID signed tool we really want to have the SMPrivilegedExecutables
# entry contain the tool's designated requirement because Xcode has built a
# more complex DR that does lots of useful and important checks. So, as a matter
2019-08-17 13:47:43 +08:00
# of policy we require that the value in SMPrivilegedExecutables match the tool's DR.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
for toolName in infoToolDict :
if infoToolDict [ toolName ] != toolNameToReqMap [ toolName ] :
raise CheckException ( " tool designated requirement ( %s ) doesn ' t match entry in ' SMPrivilegedExecutables ' ( %s ) " % ( toolNameToReqMap [ toolName ] , infoToolDict [ toolName ] ) )
def checkStep3 ( appPath , toolPathList ) :
""" Checks the " Info.plist " embedded in each helper tool. """
# First get the app's designated requirement.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
appReq = readDesignatedRequirement ( appPath , " app " )
2023-04-26 14:57:43 +08:00
# Then check that the tool's SMAuthorizedClients value matches it.
2019-08-17 13:47:43 +08:00
for toolPath in toolPathList :
info = readPlistFromToolSection ( toolPath , " __TEXT " , " __info_plist " )
2023-04-26 14:57:43 +08:00
if " CFBundleInfoDictionaryVersion " not in info or info [ " CFBundleInfoDictionaryVersion " ] != " 6.0 " :
2019-08-17 13:47:43 +08:00
raise CheckException ( " ' CFBundleInfoDictionaryVersion ' in tool __TEXT / __info_plist section must be ' 6.0 ' " , toolPath )
2023-04-26 14:57:43 +08:00
if " CFBundleIdentifier " not in info or info [ " CFBundleIdentifier " ] != os . path . basename ( toolPath ) :
2019-08-17 13:47:43 +08:00
raise CheckException ( " ' CFBundleIdentifier ' in tool __TEXT / __info_plist section must match tool name " , toolPath )
2023-04-26 14:57:43 +08:00
if " SMAuthorizedClients " not in info :
2019-08-17 13:47:43 +08:00
raise CheckException ( " ' SMAuthorizedClients ' in tool __TEXT / __info_plist section not found " , toolPath )
infoClientList = info [ " SMAuthorizedClients " ]
if not isinstance ( infoClientList , list ) :
raise CheckException ( " ' SMAuthorizedClients ' in tool __TEXT / __info_plist section must be an array " , toolPath )
if len ( infoClientList ) != 1 :
raise CheckException ( " ' SMAuthorizedClients ' in tool __TEXT / __info_plist section must have one entry " , toolPath )
2023-04-26 14:57:43 +08:00
# Again, as a matter of policy we require that the SMAuthorizedClients entry must
2019-08-17 13:47:43 +08:00
# match exactly the designated requirement of the app.
if infoClientList [ 0 ] != appReq :
raise CheckException ( " app designated requirement ( %s ) doesn ' t match entry in ' SMAuthorizedClients ' ( %s ) " % ( appReq , infoClientList [ 0 ] ) , toolPath )
def checkStep4 ( appPath , toolPathList ) :
""" Checks the " launchd.plist " embedded in each helper tool. """
for toolPath in toolPathList :
launchd = readPlistFromToolSection ( toolPath , " __TEXT " , " __launchd_plist " )
2023-04-26 14:57:43 +08:00
if " Label " not in launchd or launchd [ " Label " ] != os . path . basename ( toolPath ) :
2019-08-17 13:47:43 +08:00
raise CheckException ( " ' Label ' in tool __TEXT / __launchd_plist section must match tool name " , toolPath )
2023-04-26 14:57:43 +08:00
# We don't need to check that the label matches the bundle identifier because
# we know it matches the tool name and step 4 checks that the tool name matches
2019-08-17 13:47:43 +08:00
# the bundle identifier.
def checkStep5 ( appPath ) :
""" There ' s nothing to do here; we effectively checked for this is steps 1 and 2. """
pass
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
def check ( appPath ) :
""" Checks the SMJobBless setup of the specified app. """
# Each of the following steps matches a bullet point in the SMJobBless header doc.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
toolPathList = checkStep1 ( appPath )
checkStep2 ( appPath , toolPathList )
checkStep3 ( appPath , toolPathList )
checkStep4 ( appPath , toolPathList )
checkStep5 ( appPath )
def setreq ( appPath , appInfoPlistPath , toolInfoPlistPaths ) :
"""
2023-04-26 14:57:43 +08:00
Reads information from the built app and uses it to set the SMJobBless setup
2019-08-17 13:47:43 +08:00
in the specified app and tool Info . plist source files .
"""
if not os . path . isdir ( appPath ) :
raise CheckException ( " app not found " , appPath )
if not os . path . isfile ( appInfoPlistPath ) :
raise CheckException ( " app ' Info.plist ' not found " , appInfoPlistPath )
for toolInfoPlistPath in toolInfoPlistPaths :
if not os . path . isfile ( toolInfoPlistPath ) :
raise CheckException ( " app ' Info.plist ' not found " , toolInfoPlistPath )
# Get the designated requirement for the app and each of the tools.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
appReq = readDesignatedRequirement ( appPath , " app " )
toolDirPath = os . path . join ( appPath , " Contents " , " Library " , " LaunchServices " )
if not os . path . isdir ( toolDirPath ) :
raise CheckException ( " tool directory not found " , toolDirPath )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
toolNameToReqMap = { }
for toolName in os . listdir ( toolDirPath ) :
req = readDesignatedRequirement ( os . path . join ( toolDirPath , toolName ) , " tool " )
toolNameToReqMap [ toolName ] = req
if len ( toolNameToReqMap ) > len ( toolInfoPlistPaths ) :
raise CheckException ( " tool directory has more tools ( %d ) than you ' ve supplied tool ' Info.plist ' paths ( %d ) " % ( len ( toolNameToReqMap ) , len ( toolInfoPlistPaths ) ) , toolDirPath )
if len ( toolNameToReqMap ) < len ( toolInfoPlistPaths ) :
raise CheckException ( " tool directory has fewer tools ( %d ) than you ' ve supplied tool ' Info.plist ' paths ( %d ) " % ( len ( toolNameToReqMap ) , len ( toolInfoPlistPaths ) ) , toolDirPath )
# Build the new value for SMPrivilegedExecutables.
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
appToolDict = { }
toolInfoPlistPathToToolInfoMap = { }
for toolInfoPlistPath in toolInfoPlistPaths :
toolInfo = readInfoPlistFromPath ( toolInfoPlistPath )
toolInfoPlistPathToToolInfoMap [ toolInfoPlistPath ] = toolInfo
2023-04-26 14:57:43 +08:00
if " CFBundleIdentifier " not in toolInfo :
2019-08-17 13:47:43 +08:00
raise CheckException ( " ' CFBundleIdentifier ' not found " , toolInfoPlistPath )
bundleID = toolInfo [ " CFBundleIdentifier " ]
2023-04-26 14:57:43 +08:00
if not isinstance ( bundleID , str ) :
2019-08-17 13:47:43 +08:00
raise CheckException ( " ' CFBundleIdentifier ' must be a string " , toolInfoPlistPath )
appToolDict [ bundleID ] = toolNameToReqMap [ bundleID ]
# Set the SMPrivilegedExecutables value in the app "Info.plist".
appInfo = readInfoPlistFromPath ( appInfoPlistPath )
2023-04-26 14:57:43 +08:00
needsUpdate = " SMPrivilegedExecutables " not in appInfo
2019-08-17 13:47:43 +08:00
if not needsUpdate :
oldAppToolDict = appInfo [ " SMPrivilegedExecutables " ]
if not isinstance ( oldAppToolDict , dict ) :
raise CheckException ( " ' SMPrivilegedExecutables ' must be a dictionary " , appInfoPlistPath )
2023-04-26 14:57:43 +08:00
appToolDictSorted = sorted ( appToolDict . items ( ) , key = operator . itemgetter ( 0 ) )
oldAppToolDictSorted = sorted ( oldAppToolDict . items ( ) , key = operator . itemgetter ( 0 ) )
2019-08-17 13:47:43 +08:00
needsUpdate = ( appToolDictSorted != oldAppToolDictSorted )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
if needsUpdate :
appInfo [ " SMPrivilegedExecutables " ] = appToolDict
2023-04-26 14:57:43 +08:00
with open ( appInfoPlistPath , ' wb ' ) as fp :
plistlib . dump ( appInfo , fp )
print ( " %s : updated " % appInfoPlistPath , file = sys . stdout )
2019-08-17 13:47:43 +08:00
# Set the SMAuthorizedClients value in each tool's "Info.plist".
toolAppListSorted = [ appReq ] # only one element, so obviously sorted (-:
for toolInfoPlistPath in toolInfoPlistPaths :
toolInfo = toolInfoPlistPathToToolInfoMap [ toolInfoPlistPath ]
2023-04-26 14:57:43 +08:00
needsUpdate = " SMAuthorizedClients " not in toolInfo
2019-08-17 13:47:43 +08:00
if not needsUpdate :
oldToolAppList = toolInfo [ " SMAuthorizedClients " ]
if not isinstance ( oldToolAppList , list ) :
raise CheckException ( " ' SMAuthorizedClients ' must be an array " , toolInfoPlistPath )
oldToolAppListSorted = sorted ( oldToolAppList )
needsUpdate = ( toolAppListSorted != oldToolAppListSorted )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
if needsUpdate :
toolInfo [ " SMAuthorizedClients " ] = toolAppListSorted
2023-04-26 14:57:43 +08:00
with open ( toolInfoPlistPath , ' wb ' ) as f :
plistlib . dump ( toolInfo , f )
print ( " %s : updated " % toolInfoPlistPath , file = sys . stdout )
2019-08-17 13:47:43 +08:00
def main ( ) :
options , appArgs = getopt . getopt ( sys . argv [ 1 : ] , " d " )
2023-04-26 14:57:43 +08:00
2019-08-17 13:47:43 +08:00
debug = False
for opt , val in options :
if opt == " -d " :
debug = True
else :
raise UsageException ( )
if len ( appArgs ) == 0 :
raise UsageException ( )
command = appArgs [ 0 ]
if command == " check " :
if len ( appArgs ) != 2 :
raise UsageException ( )
check ( appArgs [ 1 ] )
elif command == " setreq " :
if len ( appArgs ) < 4 :
raise UsageException ( )
setreq ( appArgs [ 1 ] , appArgs [ 2 ] , appArgs [ 3 : ] )
else :
raise UsageException ( )
if __name__ == " __main__ " :
try :
main ( )
2023-04-26 14:57:43 +08:00
except CheckException as e :
2019-08-17 13:47:43 +08:00
if e . path is None :
2023-04-26 14:57:43 +08:00
print ( " %s : %s " % ( os . path . basename ( sys . argv [ 0 ] ) , e . message ) , file = sys . stderr )
2019-08-17 13:47:43 +08:00
else :
path = e . path
if path . endswith ( " / " ) :
path = path [ : - 1 ]
2023-04-26 14:57:43 +08:00
print ( " %s : %s " % ( path , e . message ) , file = sys . stderr )
2019-08-17 13:47:43 +08:00
sys . exit ( 1 )
2023-04-26 14:57:43 +08:00
except UsageException as e :
print ( " usage: %s check /path/to/app " % os . path . basename ( sys . argv [ 0 ] ) , file = sys . stderr )
print ( " %s setreq /path/to/app /path/to/app/Info.plist /path/to/tool/Info.plist... " % os . path . basename ( sys . argv [ 0 ] ) , file = sys . stderr )
sys . exit ( 1 )