Apparency

Automating with AppleScript

Apparency is a scriptable application, which means that you can automate it using AppleScript.

The Apparency scripting dictionary is thoroughly documented, and contains many bits of sample code. You can open the scripting dictionary directly from Script Editor, of course; or from Apparency, use Help > Open Scripting Dictionary. the Apparency scripting dictionary

If you're using AppleScript in any significant way, we highly recommend checking out the venerable Script Debugger application. While you can get by with the built-in Script Editor application, Script Debugger is more capable and better designed in every way — yes, it costs Actual Money, but it's well worth it.
In addition to AppleScript, there are other scripting dialects that are built upon the same Open Scripting Architecture (OSA).

JavaScript for Automation (JXA) was introduced back in OS X 10.10 (Yosemite), and allows you to use modern JavaScript for communicating with scriptable applications. On the upside, you get the built-in capabilities of the JavaScript language, including all of the improvements of the last decade, because JXA is built on the same JavaScriptCore engine that underlies Safari. On the downside, JXA itself seems to have been largely neglected in the last decade — although it does still broadly work. Apple has done nothing with JXA documentation since OS X 10.11 (El Capitan), but this site by Christian Kirsch is a fantastic resource.

Alternatively, the Scripting Bridge was introduced back in Mac OS X 10.5 (Leopard), and allows you to use Objective-C for communicating with scriptable applications. For the shrinking number of us that still like Objective-C, the appeal is obvious. But the scripting bridge seems to be neglected about as much as JXA. The API is still described in the active documentation, but important conceptual information — such as how to generate bridging headers — is relegated to the archive.

A few examples of how to use Apparency via AppleScript follow. We've also translated them into the JavaScript for Automation (JXA) dialect; use the buttons at the top of each example to switch between languages.

This first script demonstrates how to open an application in Apparency, and how to get at some of the basic attributes of the app, such as the supported processor architectures, macOS version requirements, and code signature status. This script also demonstrates how you can extract the app icon (with a specific image format and size) and write that to a file to use elsewhere (here, we show it in the display dialog alerts):

tell application "Apparency"
-- open app by its POSIX path
set theDocument to (open "/Applications/NetNewsWire.app")
-- get the top-level component
set theApp to root component of theDocument
-- and its icon as a PNG file (see handler below)
set iconFile to my imageFileForComponentIcon(theApp)
-- does it support Apple Silicon Macs?
if not (theApp supports processor arm64e) then
display dialog (get name of theApp) & " doesn't support Apple Silicon" with icon iconFile giving up after 1
end if
-- does OS support go back to at least macOS 12 (Monterey)?
set req to OS minimum version of theApp
if req is not missing value and platform of req is macOS then
-- use the "check OS version" command to easily compare against version we need
if not (check OS version req is before or at "12") then
display dialog (get name of theApp) & " requires at least macOS " & (product version of req) with icon iconFile giving up after 1
end if
end if
-- examine the dylibs linked by the active executable
set exec to active executable slice of theApp
if exec is not missing value then -- i.e. it has executable code
if (linked libraries of exec) contains {"/System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI"} then
display dialog (get name of theApp) & " links to SwiftUI for architecture " & (name of exec) with icon iconFile giving up after 1
end if
end if
-- examine the trust of the code signature on the active executable;
-- note we always get an «active code signature» but it might have «signed» as false
set sig to active code signature of theApp
if sig is not signed or sig is not trusted or sig is not verified then
display dialog (get name of theApp) & " has a missing, untrusted or broken signature" giving up after 1
end if
if signing certificate category of sig is Developer ID signing then
set theCert to signing certificate of sig
display dialog (get name of theApp) & " is Developer ID signed by " & (organization name of subject of theCert) with icon iconFile giving up after 1
end if
-- check for an arbitrary-load ATS exception;
-- Apparency provides the «Info plist path» to locate the Info.plist for any component,
-- and then we can use the Property List Suite from System Events to examine it
set plistPath to Info plist path of theApp
if plistPath is not missing value then -- i.e. there is an Info.plist for this component
tell application "System Events"
try -- assume any error in trying to get the right key means it's not defined
set allowArbitrary to property list item "NSAllowsArbitraryLoads" of property list item "NSAppTransportSecurity" of property list file plistPath
if allowArbitrary is not missing value and value of allowArbitrary is true then
display dialog (get name of theApp) & " allows arbitrary loads via ATS" with icon iconFile giving up after 1
end if
end try
end tell
end if
-- clean up
close theDocument
end tell
---------------------------------------------------------------------------------------------------
-- imageFileForComponentIcon()
--
-- handler to get the component's icon and save as a (temporary) PNG file
---------------------------------------------------------------------------------------------------
on imageFileForComponentIcon(theComponent)
tell application "Apparency"
tell theComponent
set asData to make icon image data as PNG dimensions {256, 256} scale factor 2.0
if asData is not missing value then
tell AppleScript
set outputPath to (path to temporary items as string) & (get name of theComponent) & ".png"
try
set outFileNum to (open for access outputPath with write permission)
write asData to outFileNum
on error number n
error "Failed to write icon data to " & outputPath & "(error " & n & ")"
end try
close access outFileNum
return alias outputPath -- wrote the icon to a file, return as an alias
end tell
end if
-- if we couldn't get the icon, return a placeholder image file
return POSIX file "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/UnknownFSObjectIcon.icns" as alias
end tell
end tell
end imageFileForComponentIcon
// get access to Standard Additions
var Standard = Application.currentApplication();
Standard.includeStandardAdditions = true;
// get access to Apparency
var Apparency = Application( 'Apparency' );
// get path to the app and tell Apparency to open it
var theAppPath = Path( '/Applications/NetNewsWire.app' );
var theDocument = Apparency.open( theAppPath );
// get the top-level component
var theApp = theDocument.rootComponent();
// and its icon as a PNG file (see function below), to use in any dialogs
var iconFile = imageFileForComponentIcon( theApp );
// does it support Apple Silicon Macs?
if ( ! Apparency.supports( theApp, { processor : 'arm64e' } ) )
Standard.displayDialog( theApp.name() + " doesn't support Apple Silicon", { withIcon : iconFile, givingUpAfter : 1 } );
// does OS support go back to at least macOS 12 (Monterey)?
var req = theApp.osMinimumVersion();
if ( req && req.platform == 'macOS' )
{
if ( ! Apparency.checkOSVersion( req, { isBeforeOrAt : '12' } ) )
Standard.displayDialog( theApp.name() + " requires at least macOS " + req.productVersion, { withIcon : iconFile, givingUpAfter : 1 } );
}
// examine the dylibs linked by the active executable
var exec = theApp.activeExecutableSlice();
if ( exec ) // i.e. it has executable code
{
if ( exec.linkedLibraries().includes( '/System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI' ) )
Standard.displayDialog( theApp.name() + " links to SwiftUI for architecture " + exec.name(), { withIcon : iconFile, givingUpAfter : 1 } );
}
// examine the trust of the code signature on the active executable;
// note we always get an «active code signature» but it might have «signed» as false
var sig = theApp.activeCodeSignature();
if ( ! sig.signed() || ! sig.trusted() || ! sig.verified() )
Standard.displayDialog( theApp.name() + " has a missing, untrusted or broken signature", { withIcon : iconFile, givingUpAfter : 1 } );
if ( sig.signingCertificateCategory() == 'Developer ID signing' )
{
var theCert = sig.signingCertificate();
Standard.displayDialog( theApp.name() + " is Developer ID signed by " + theCert.subject().organizationName(), { withIcon : iconFile, givingUpAfter : 1 } );
}
// check for an arbitrary-load ATS exception;
// Apparency provides the «Info plist path» to locate the Info.plist for any component,
// and then we can use the Property List Suite from System Events to examine it
var plistPath = theApp.infoPlistPath();
if ( plistPath ) // i.e. there is an Info.plist for this component
{
// This is the magic incantation by which we ask System Events to load the plist and
// turn it into a JSON-style object representation. (Don't try to make a propertyListFile
// object, or fiddle with PropertyListItems, both of which seem hopeless.)
var plist = Application( 'System Events' ).propertyListFiles.byName( plistPath ).value();
// Use object notation (along with optional chaining and null coalescing) to query a key-path
if ( plist.NSAppTransportSecurity?.NSAllowsArbitraryLoads ?? false )
Standard.displayDialog( theApp.name() + " allows arbitrary loads via ATS", { withIcon : iconFile, givingUpAfter : 1 } );
}
// clean up
theDocument.close();
///////////////////////////////////////////////////////////////////////////////
// imageFileForComponentIcon()
//
// function to get the component's icon and save as a (temporary) PNG file
///////////////////////////////////////////////////////////////////////////////
function imageFileForComponentIcon( theComponent )
{
var Apparency = Application( 'Apparency' );
// note that we request a base-64 encoded string, since JXA doesn't
// seem to be able to do anything useful with a raw data value
var b64 = Apparency.makeIconImageData( theComponent, {
as : 'PNG',
dimensions : [ 256, 256 ],
scaleFactor : 2.0,
usingBase64Encoding : true,
} );
// construct temporary path to write the image data
var outputPath = Path( Standard.pathTo( 'temporary items' ) + '/' + theComponent.name() + '.png' );
// use the Objective-C bindings to convert the base-64 string back into raw data,
// and then write that out to the file
var asData = $.NSData.alloc.initWithBase64EncodedStringOptions( b64, 0 );
if ( asData.writeToFileAtomically( outputPath.toString(), false ) )
return outputPath;
// if we couldn't make the file for any reason, return a placeholder image file
return Path( '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/UnknownFSObjectIcon.icns' );
}

The above example examines only the top-level app, but many (if not most) apps contain various sub-components. You can traverse the component hierarchy explicitly from AppleScript, by asking for the components of the root component, and continuing onward until you reach the bottom — this is most naturally done with a recursive handler.

However, most of the time, you won't be interested in the structure of the hierarchy, so Apparency also provides a find components command, which can be used to easily get all of the components at once, in a single list. This script uses the find components command to check all the components of an app against some sort of local policy — such as that everything must be Developer ID-signed and notarized:

tell application "Apparency"
-- open app and look in de facto code places for additional components
set theDocument to (open "/Applications/NetNewsWire.app" with looking in de facto code places)
-- use «find components» command to loop over every component, regardless of hierarchy;
-- note that we need to use «under components of theDocument» here to find all components,
-- including the root one («under root component» gets misinterpreted by AppleScript in this context)
repeat with aComponent in (find components under components of theDocument)
-- use the UTI to examine only components that *could* be signed: bundles or Mach-O binaries
if (UTI of aComponent conforms to "com.apple.bundle") ¬
or (UTI of aComponent conforms to "com.apple.mach-o-binary") then
-- get icon as a PNG file to use in dialogs (see handler definition above)
set iconFile to my imageFileForComponentIcon(aComponent)
-- every supported architecture has its own code signatures: let's check all of them
repeat with aSig in code signatures of aComponent
if aSig is not signed then
display dialog (get name of aComponent) & " (" & (get name of aSig) & ") is not signed" with icon iconFile giving up after 1
else if not (aSig is trusted and signing certificate category of aSig is Developer ID signing) then
display dialog (get name of aComponent) & " (" & (get name of aSig) & ") doesn't have a trusted Developer ID signature" with icon iconFile giving up after 1
else if aSig is not verified then
display dialog (get name of aComponent) & " (" & (get name of aSig) & ") didn't verify" with icon iconFile giving up after 1
else if aSig is not notarized then
display dialog (get name of aComponent) & " (" & (get name of aSig) & ") wasn't notarized" with icon iconFile giving up after 1
end if
end repeat
end if
end repeat
-- clean up
close theDocument
end tell
// get access to Standard Additions
var Standard = Application.currentApplication();
Standard.includeStandardAdditions = true;
// get access to Apparency
var Apparency = Application( 'Apparency' );
// open app and look in de facto code places for additional components
var theAppPath = Path( '/Applications/NetNewsWire.app' );
var theDocument = Apparency.open( theAppPath, { lookingInDeFactoCodePlaces : true } );
// use «find components» command to loop over every component, regardless of hierarchy;
// we can specify «under» as the rootComponent(), which will find all components,
// including the root one (this works in JXA, even though it wouldn't work in AppleScript)
var allComponents = Apparency.findComponents( { under : theDocument.rootComponent() } );
allComponents.forEach( function( aComponent )
{
// use the UTI to examine only components that *could* be signed: bundles or Mach-O binaries
if ( Apparency.conforms( aComponent.uti(), { to : 'com.apple.bundle' } )
|| Apparency.conforms( aComponent.uti(), { to : 'com.apple.mach-o-binary' } ) )
{
// get icon as a PNG file to use in any dialogs (see function definition above)
var iconFile = imageFileForComponentIcon( aComponent );
// every supported architecture has its own code signatures: let's check all of them
aComponent.codeSignatures().forEach( function( aSig )
{
if ( ! aSig.signed() )
Standard.displayDialog( aComponent.name() + " (" + aSig.name() + ") is not signed", { withIcon : iconFile, givingUpAfter : 1 } );
else if ( ! ( aSig.trusted() && aSig.signingCertificateCategory() == 'Developer ID signing' ) )
Standard.displayDialog( aComponent.name() + " (" + aSig.name() + ") doesn't have a trusted Developer ID signature", { withIcon : iconFile, givingUpAfter : 1 } );
else if ( ! aSig.verified() )
Standard.displayDialog( aComponent.name() + " (" + aSig.name() + ") didn't verify", { withIcon : iconFile, givingUpAfter : 1 } );
else if ( ! aSig.notarized() )
Standard.displayDialog( aComponent.name() + " (" + aSig.name() + ") wasn't notarized", { withIcon : iconFile, givingUpAfter : 1 } );
} );
}
} );
// clean up
theDocument.close();

As an alternative to the above, it can be more succinct to apply a standard AppleScript whose clause to the find components command. You can even use uniform type identifiers (UTIs) to look for components of certain kinds:

tell application "Apparency"
-- use the handler defined below to get/make the necessary UTIs
set bundleType to my getOrMakeUTI("com.apple.bundle")
set machOType to my getOrMakeUTI("com.apple.mach-o-binary")
-- use «find components» with a «whose» clause to find only those components that meet our criteria;
-- we use the UTIs created above to test conformance: «UTI is in» means «UTI conforms to» in this context
repeat with aComponent in (find components under components of theDocument ¬
where (UTI is in bundleType or UTI is in machOType) ¬
and (signed of active code signature is false ¬
or trusted of active code signature is false ¬
or signing certificate category of active code signature is not Developer ID signing ¬
or notarized of active code signature is false))
-- found a component that doesn't meet criteria: report it (with its icon: see handler definition above)
set iconFile to my imageFileForComponentIcon(aComponent)
display dialog (get name of aComponent) & " doesn't meet our requirements" with icon iconFile giving up after 1
end repeat
end tell
-- handler to find or make the UTI if necessary
on getOrMakeUTI(utiName)
tell application "Apparency"
if uniform type ID utiName of theDocument exists then
return uniform type ID utiName of theDocument
else
tell theDocument to return make new uniform type ID with properties {name:utiName}
end if
end tell
end getOrMakeUTI
/*
Unfortunately, we haven't been able to make this example work in the JavaScript for
Automation dialect. The JXA "array filtering" syntax doesn't provide any way to express
an "is in" test, so we can't directly do the equivalent of this AppleScript:
where UTI is in machOType
JXA provides for a "contains" test, which is the reverse of "is in", but we've had no
luck getting this to work. It ought to be something like:
whose( { _match: [ machOType, { _contains: ObjectSpecifier().uti } ] } )
but JXA doesn't seem to like the first operand of a match not being a property on the
object being tested (i.e. a component in this case). It only throws an obtuse error,
and never even sends an AppleEvent to Apparency.
See the previous example for how to use an unqualified findComponents command, and
then do UTI testing via an explicit conforms command.
*/

We end with a more complex example, where we use Apparency to validate an app prior to distribution, checking for certain attributes that we want all components to have upon release, and using Numbers to create a summary report:

property numbersDoc : missing value
property numbersTable : missing value
property nextRow : 1
on run
-- create a new Numbers document to hold our results
tell application "Numbers"
activate
set numbersDoc to (make new document with properties {name:"Validation"})
tell numbersDoc
tell first sheet to delete first table -- get rid of the default table
tell first sheet to set numbersTable to (make new table with properties {column count:7, row count:2, name:"Validation"})
end tell
my addRow("Component", {"Identifier", "Version", "Bundle Version", "Architectures", "Required OS", "Code Signature"})
end tell
-- grab the icon from the root component (see handler definition above), and insert next to the table
tell application "Apparency" to set appIconFile to my imageFileForComponentIcon(get root component of theDocument)
tell application "Numbers"
set p to position of numbersTable -- top-left corner of the table
set item 1 of p to ((item 1 of p) + (get width of numbersTable) + 20) -- insert image to right of the table
tell first sheet of numbersDoc to make image at end of images with properties {file:appIconFile, position:p}
end tell
-- use Apparency to validate each component, starting from the root one
tell application "Apparency"
repeat with aComponent in (find components under components of theDocument)
my validateComponent(aComponent)
end repeat
end tell
end run
-- handler to validate a single component and send results to a new row in the Numbers table
on validateComponent(theComponent)
tell application "Apparency"
-- this list holds the columns of our results for this component
set r to {}
-- get the identifier and version info
set r to r & {bundle identifier of theComponent}
set r to r & {bundle version of theComponent}
set r to r & {internal bundle version of theComponent}
---- do we support the expected architectures (if there's any executable at all)?
set okay to "❌"
if ((count of every executable slice of theComponent) is 0) ¬
or (theComponent supports processor arm64e) and (theComponent supports processor x86_64) then
set okay to "✅"
end if
-- «supported architectures» returns a list, so separate them by commas
set {saveTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {", "}}
set r to r & {okay & " " & (supported architectures of theComponent) as string}
set AppleScript's text item delimiters to saveTID
-- do we support back to to the expected OS version?
set minOS to OS minimum version of theComponent
set okay to "❌"
if minOS is not missing value and (check OS version minOS is before or at "11.0") then
set okay to "✅"
end if
set r to r & {okay & " " & (product version of minOS)}
-- is code validly signed with our expected Developer ID identity?
set sig to active code signature of theComponent
set okay to "❌"
if signed of sig is true and trusted of sig is true and verified of sig is true ¬
and signing certificate category of sig is Developer ID signing ¬
and team ID of sig is "936EB786NH" then
set okay to "✅"
end if
set r to r & {(okay & " " & (signing certificate category of sig) as string) & " (" & team ID of sig & ")"}
-- done with result for this component, so add as new row in the Numbers table
my addRow((get name of theComponent), r)
end tell
end validateComponent
-- handler to add a new row to the Numbers table we made on init
on addRow(rowTitle, columnValues)
tell application "Numbers"
-- the nextRow property holds the next available row to populate, so make
-- sure that we have at least that many rows available
if row count of numbersTable < nextRow then
tell numbersTable to (make new row with properties {address:nextRow})
end if
-- now populate the row with the given title (in column A) and values (remaining columns)
tell row nextRow of numbersTable
set value of cell 1 to rowTitle
repeat with dataColumn from 2 to (count of columnValues) + 1
set value of cell dataColumn to item (dataColumn - 1) of columnValues
end repeat
end tell
-- advance nextRow property to point to next available row
set nextRow to nextRow + 1
end tell
end addRow
var numbersDoc;
var numbersTable;
var nextRow = 1;
// create a new Numbers document to hold our results
var Numbers = Application( 'Numbers' );
Numbers.activate();
numbersDoc = Numbers.Document( { name : 'Validation' } );
Numbers.documents.push( numbersDoc );
Numbers.delete( numbersDoc.sheets.at( 0 ).tables.at(0 ) );
numbersTable = Numbers.Table( { columnCount : 7, rowCount : 2, name : 'Validation' } );
numbersDoc.sheets.at( 0 ).tables.push( numbersTable );
addRow( "Component", [ "Identifier", "Version", "Bundle Version", "Architectures", "Required OS", "Code Signature" ] );
// grab the icon from the root component (see function above), and insert next to the table
var iconFile = imageFileForComponentIcon( theDocument.rootComponent() );
var p = numbersTable.position(); // top-left corner of the table
p.x = ( p.x + numbersTable.width() + 20 ); // insert image to right of the table
var numbersImage = Numbers.Image( { file : iconFile, position : p } );
numbersDoc.sheets.at( 0 ).images.push( numbersImage );
// use Apparency to validate each component, starting from the root one
Apparency.findComponents( { under : theDocument.rootComponent() } ).forEach( validateComponent );
// function to validate a single component and send results to a new row in the Numbers table
function validateComponent( theComponent )
{
// this array holds the columns of our results for this components
var r = Array();
// get the identifier and version info
r.push( theComponent.bundleIdentifier() );
r.push( theComponent.bundleVersion() );
r.push( theComponent.internalBundleVersion() );
// do we support the expected architectures (if there's any executable at all)?
var okay = "❌";
if ( theComponent.executableSlices().length == 0 // i.e. no executable
|| ( Apparency.supports( theComponent, { processor : 'arm64e' } )
&& Apparency.supports( theComponent, { processor : 'x86_64' } ) ) )
okay = "✅";
r.push( okay + " " + theComponent.supportedArchitectures().join( ', ' ) );
// do we support back to to the expected OS version?
var minOS = theComponent.osMinimumVersion();
okay = "❌";
if ( minOS && Apparency.checkOSVersion( minOS, { isBeforeOrAt: "11.0" } ) )
okay = "✅";
r.push( okay + " " + minOS.productVersion );
// is code validly signed with our expected Developer ID identity?
var sig = theComponent.activeCodeSignature();
okay = "❌";
if ( sig.signed() && sig.trusted() && sig.verified()
&& sig.signingCertificateCategory() == 'Developer ID signing'
&& sig.teamID() == '936EB786NH' )
okay = "✅";
r.push( okay + " " + sig.signingCertificateCategory() + " (" + sig.teamID() + ")" );
// done with result for this component, so add as new row in the Numbers table
addRow( theComponent.name(), r );
}
// function to add a new row to the Numbers table we made above
function addRow( rowTitle, columnValues )
{
// the nextRow property holds the next available row to populate, so make
// sure that we have at least that many rows available
if ( numbersTable.rowCount() < nextRow )
numbersTable.rows.push( Numbers.Row( { address : nextRow } ) );
// now populate the row with the given title (in column A) and values (remaining columns)
var theRow = numbersTable.rows.at( nextRow - 1 );
theRow.cells.at( 0 ).value = rowTitle;
for ( var dataColumn = 1 ; dataColumn < ( columnValues.length + 1 ) ; ++dataColumn )
theRow.cells.at( dataColumn ).value = columnValues.at( dataColumn - 1 );
// advance nextRow property to point to next available row
++nextRow;
}

The above examples give a flavor of using AppleScript with Apparency, but there are many other properties and elements you can use. Again, check out the scripting dictionary for more information and many more examples.