Deploying Libraries

Apple introduced its script library loading system in OS X 10.9, something that was long overdue. The system provides a choice of locations for script libraries, and with choice comes the need to make decisions. Here are some suggested approaches.

First, a summary of how library loading works. You reference a script with a use script statement, followed by its name (with or without extension). AppleScript will then look for the script to load at run-time. Unlike the library mechanism used in Script Debugger, script libraries are not embedded in your script by default.

AppleScript follows a strict search order. If the script containing the use statement is a bundle — an applet or .scptd file — it first looks within the bundle itself, inside /Contents/Resources/Script Libraries. If it does not find it there, it looks in the user library, in ~/Library/Script Libraries. If it doesn’t find it there, it looks in /Library, then /Network/Library, then /System/Library.

In OS X 10.11 and later, it looks in two other places. You can use an environment variable of OSA_LIBRARY_PATH to define a path that is searched after the script’s bundle (not something most people will ever do). And if none of the above are successful, it will also search the bundles of all known applications.

TIP: Don’t use subfolders in Script Libraries folders. They may work, but they are unsupported and may stop working at some point in the future.

So where should you keep your script libraries? That depends on how you deploy your scripts.

If you are writing scripts for your own use, the best choice is likely to be ~/Library/Script Libraries/. It means you have a single copy to edit if you need to make changes, and there are no permission issues involved, as there is with /Library/Script Libraries. It is pretty simple.

If you write scripts for a small group of people, say for a group of co-workers, you might want a different approach. You probably distribute scripts as run-only in this case, and it might be more difficult to update libraries. Your choice is also going to depend on whether the contents of libraries are likely to change often.

If updating shared script libraries is difficult, or if you deploy scripts more widely, the best choice is to embed them within the scripts you distribute. If you normally distribute .scpt files, you should be able to use .scptd files instead, so you can embed the libraries. But you probably do not want to embed script libraries while you are developing and testing scripts.

So a practical workflow might be this: on your development machine you have the editable libraries in ~/Library/Script Libraries, and obviously the scripts that use them are in editable form. When you wish to distribute the scripts, you can embed the libraries, export as a run-only applet or .scptd bundle, choosing the Make bundled scripts & libraries run-only option, and then hopefully remember to remove the embedded versions when you have finished.

Export run-only

The Export Run-Only dialog, showing the Make bundled scripts & libraries run-only option.

That’s a bit tedious, but fortunately the process can be scripted. Here is a sample script you can adapt to your needs:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
use application id "com.latenightsw.ScriptDebugger6" version "6.0.1" without importing -- script requires version 6.0.1 or later

property codesignID : missing value
property chooseFlag : true -- whether to use a fixed name or show a save dialog

set myPath to path to me as text
tell application id "com.latenightsw.ScriptDebugger6" -- Script Debugger.app
    -- stop script from trying to export itself when testing
    if myPath = (get file spec of front document as text) then
        set docNum to 2
    else
        set docNum to 1
    end if
    -- get info from document
    tell document docNum
        if script type is not in {script application, bundled compiled script} then -- it's not a bundle
            beep
            display dialog "This document is neither a script bundle nor an applet, so script libraries cannot be bundled within it." buttons {"OK"} default button "OK"
            error number -128
        end if
        set theResult to compile with showing errors
        if not theResult then -- it didn't compile; go no further
            error number -128
        end if
        set usedLibFiles to used script library files
        set libCount to count of usedLibFiles
        if libCount is 0 then -- no libs, nothing to do
            beep
            display dialog "This document does not use any script libraries." buttons {"OK"} default button "OK"
            error number -128
        end if
        set theFile to file spec
        set scriptType to script type
        set showStartup to show startup
        set stayOpen to stay open
    end tell
end tell

-- build new file path
set filePath to current application's NSString's stringWithString:(POSIX path of theFile)
set theExt to filePath's pathExtension()
set nameLessExt to filePath's lastPathComponent()'s stringByDeletingPathExtension()
set newName to current application's NSString's stringWithFormat_("%@ Run Only.%@", nameLessExt, theExt)
if chooseFlag then -- show dialog
    set folderPath to filePath's stringByDeletingLastPathComponent() as text as POSIX file
    set newFile to (choose file name with prompt "Export a run-only version of the script in:" default name (newName as text) default location folderPath)
else
    set newPath to filePath's stringByDeletingLastPathComponent()'s stringByAppendingPathComponent:newName
    set newFile to newPath as text as POSIX file
end if

-- create folder in bundle if necessary
set fileManager to current application's NSFileManager's defaultManager()
set libsFolderPath to (POSIX path of theFile) & "Contents/Resources/Script Libraries"
set libsFolderURL to current application's |NSURL|'s fileURLWithPath:libsFolderPath
fileManager's createDirectoryAtURL:libsFolderURL withIntermediateDirectories:true attributes:(missing value) |error|:(missing value)

-- copy files
set copiedFiles to {}
repeat with aFile in usedLibFiles
    set aLibURL to (current application's |NSURL|'s fileURLWithPath:(POSIX path of aFile))
    set libName to aLibURL's lastPathComponent()
    set newLibURL to (libsFolderURL's URLByAppendingPathComponent:libName)
    if not (newLibURL's isEqual:aLibURL) as boolean then -- check it's not already an embedded lib
        -- remove existing version if any
        (fileManager's removeItemAtURL:newLibURL |error|:(missing value))
        -- copy into bundle
        set {theResult, theError} to (fileManager's copyItemAtURL:aLibURL toURL:newLibURL |error|:(reference))
        if not theResult as boolean then error "Could not copy library to bundle: " & (localizedDescription() as text) from me
        set end of copiedFiles to newLibURL
    end if
end repeat

-- export run-only version
tell application id "com.latenightsw.ScriptDebugger6" -- Script Debugger.app
    activate
    tell document docNum
        set oldID to codesign identity
        set codesign identity to codesignID
        -- force recompile with bundled libs
        set selection to {1, 0}
        set selection to space
        compile
        save in newFile as scriptType stay open stayOpen show startup screen showStartup with run only and bundle run only
        set codesign identity to oldID
    end tell
end tell

-- remove the libraries we added
repeat with aFile in copiedFiles
    set {theResult, theError} to (fileManager's removeItemAtPath:aFile |error|:(reference))
    if not theResult as boolean then error "Could not delete library from bundle: " & (localizedDescription() as text) from me
end repeat

-- recompile
tell application id "com.latenightsw.ScriptDebugger6" -- Script Debugger.app
    activate
    tell document docNum
        -- force recompile without bundled libs
        set selection to {1, 0}
        set selection to space
        compile
    end tell
    display dialog "Done." buttons {"OK"} default button 1
end tell

Some comments on the script:

  • The script requires Script Debugger 6.0.1, which introduced the bundle run only parameter to the save command. This is enforced by the line:

use application id "com.latenightsw.ScriptDebugger6" version "6.0.1" without importing

  • If you run the script with Script Debugger as the host application, say from its Script menu, the without importing is irrelevant. Indeed, you can also skip the Script Debugger tell blocks. But as a general rule, if you import terminology via use application statements you are opening yourself to lots of potential terminology clashes — and you also make it harder for Script Debugger to help you debug problems. So this form is used in this script more as a matter of good practice: use application statements make good sense as a way of enforcing version requirements, but not as a way of loading terminology.

  • The codesignID property should be changed to your identity if you want to sign the exported app. This lets you limit signing just to your exported version. A value of missing value means the exported script will not be signed. If you are using Script Debugger’s always-on codesigning facility, you should remove the property and relevant code.

  • The chooseFlag property controls whether you get a dialog asking you for the name and destination of the exported file, or whether to put it in the same directory as the original, appending ” Run Only” before the extension. Change its value to suit.

  • The script uses AppleScriptObjC to do the file handling. You could use the Finder or System Events, but Cocoa’s path- and file-related facilites made it a more appealling choice.