Embedding script libraries that call other libraries

When a script contains a use script statement, AppleScript follows some pretty simple rules for where to look for the script library when compiling.

If the host script is an applet or script bundle, the first port of call is to check inside the bundle’s /Contents/Resources/Script Libraries folder, if there is one. If that comes up empty, or the script is not a bundle, the next stop is ~/Library/Script Libraries, followed by /Library/Script Libraries. In 10.11 or later, it will also look for libraries included in other applications (in their /Contents/Library/Script Libraries folder, not in /Contents/Resources/Script Libraries).

However, things get tricky is when one script library uses another library.

Consider the case of two script libraries, Lib A and Lib B, where Lib A also uses Lib B. Now consider an applet that uses both. As long as both scripts are in one of the folders searched by all scripts — usually ~/Library/Script Libraries — all will be fine.

Suppose you want to distribute the applet, so you embed copies of Lib A and Lib B in your applet, in its /Contents/Resources/Script Libraries folder. Your main script will find them there, but what of Lib A’s dependence in Lib B? Lib A knows nothing of the bundle it is embedded in, but as long as it can see a copy in ~/Library/Script Libraries, it will compile fine.

Now what happens if the embedded libraries are the only copies — either the applet has been moved to a different Mac, or the copies in ~/Library/Script Libraries have been removed?

The good news is that the applet will generally run fine, because the search rules for running a script are slightly different to those when compiling: Lib A will first check to see if Lib B has already been loaded, which it will have been by the applet, so there will be no need to search any further. And of course you can edit the applet script fine.

What you can’t do, though, is edit Lib A as long it depends on Lib B and it can’t find a copy of Lib B in its search path. The fact that both are part of a larger bundle is irrelevant.

The best way to handle this is to try to avoid it. Edit and build scripts with libraries in ~/Library/Script Libraries, and only bundle them when you are about to deploy, preferably in run-only form. But if you cannot do that for some reason, you can work around the issue using symlinks.

Doing this requires that your libraries are saved as bundles (.scptd files), not simple .scpt files. Then you can put relative symlinks inside the bundles’ /Contents/Resources/Script Libraries folders pointing to the other libraries.

It’s a bit fiddly to attempt manually, but something scripting can do simply, as below. Place the script in Script Debugger’s Scripts menu, and run it with the applet open.

use AppleScript version "2.5" -- macOS 10.11 or later
use framework "Foundation"
use scripting additions

-- classes, constants, and enums used
property NSFileManager : a reference to current application's NSFileManager
property NSPredicate : a reference to current application's NSPredicate
property NSURL : a reference to current application's NSURL

tell application id "com.latenightsw.ScriptDebugger7" -- Script Debugger.app
	set thePath to file spec of document 1 -- change to suit if testing
end tell
-- get a list of all embedded libraries
set mainLibURL to NSURL's fileURLWithPath:"Contents/Resources/Script Libraries" isDirectory:true relativeToURL:thePath
set fileManager to NSFileManager's defaultManager()
set theURLs to (fileManager's contentsOfDirectoryAtURL:mainLibURL includingPropertiesForKeys:(missing value) options:0 |error|:(missing value))'s allObjects()
set bundleLibs to theURLs's filteredArrayUsingPredicate:(NSPredicate's predicateWithFormat:"pathExtension == 'scptd'")
set allLibs to theURLs's filteredArrayUsingPredicate:(NSPredicate's predicateWithFormat:"pathExtension == 'scptd' OR pathExtension == 'scpt'")
set allLibNames to allLibs's valueForKeyPath:"lastPathComponent"
-- loop through bundles
repeat with aBundle in bundleLibs
	-- make sure they have their own Script Libraries folders
	set subLibURL to (NSURL's fileURLWithPath:"Contents/Resources/Script Libraries" isDirectory:true relativeToURL:aBundle)
	(fileManager's createDirectoryAtURL:subLibURL withIntermediateDirectories:true attributes:(missing value) |error|:(reference))
	set bundleNameLessExt to aBundle's lastPathComponent()'s stringByDeletingPathExtension()
	-- loop through library names
	repeat with libName in allLibNames
		set libNameLessExt to libName's stringByDeletingPathExtension()
		if not (bundleNameLessExt's isEqualToString:libNameLessExt) as boolean then -- don't link to self
			-- make relative symbolic link to the library; will silently fail if already there
			(fileManager's createSymbolicLinkAtPath:(subLibURL's |path|()'s stringByAppendingPathComponent:libName) withDestinationPath:("../../../../" & (libName as text)) |error|:(missing value))
		end if
	end repeat
end repeat

Make sure you save after running the script. It uses a bit of brute force – it puts links whether needed or not – but unused links don’t matter.

If you want to remove the links, perhaps to modify the scripts significantly or use them elsewhere, this script will do the job:

use AppleScript version "2.5" -- macOS 10.11 or later
use framework "Foundation"
use scripting additions

-- classes, constants, and enums used
property NSPredicate : a reference to current application's NSPredicate
property NSURL : a reference to current application's NSURL
property NSFileManager : a reference to current application's NSFileManager
property NSURLIsSymbolicLinkKey : a reference to current application's NSURLIsSymbolicLinkKey

tell application id "com.latenightsw.ScriptDebugger7" -- Script Debugger.app
	set thePath to file spec of document 1 -- change to suit if testing
end tell
-- get a list of all embedded library bundles
set mainLibURL to NSURL's fileURLWithPath:"Contents/Resources/Script Libraries" isDirectory:true relativeToURL:thePath
set fileManager to NSFileManager's defaultManager()
set theURLs to (fileManager's contentsOfDirectoryAtURL:mainLibURL includingPropertiesForKeys:(missing value) options:0 |error|:(missing value))'s allObjects()
set bundleLibs to theURLs's filteredArrayUsingPredicate:(NSPredicate's predicateWithFormat:"pathExtension == 'scptd'")
-- loop through bundles
repeat with aBundle in bundleLibs
	-- see if they have their own Script Libraries folders
	set subLibURL to (NSURL's fileURLWithPath:"Contents/Resources/Script Libraries" isDirectory:true relativeToURL:aBundle)
	if (subLibURL's checkResourceIsReachableAndReturnError:(missing value)) as boolean then
		-- get sub-library URLs
		set subLibURLs to (fileManager's contentsOfDirectoryAtURL:subLibURL includingPropertiesForKeys:{NSURLIsSymbolicLinkKey} options:0 |error|:(missing value))'s allObjects()
		-- loop through sub-libraries
		repeat with aSubLib in subLibURLs
			-- check if it's a symlink and if so delete it
			set {theResult, theValue} to (aSubLib's getResourceValue:(reference) forKey:(NSURLIsSymbolicLinkKey) |error|:(missing value))
			if theValue as boolean then
				(fileManager's removeItemAtURL:aSubLib |error|:(missing value))
			end if
		end repeat
	end if
end repeat
8 Likes

Thanks, Shane. This is very help. Evernoted for future ref.

1 Like

My goodness you have just saved me! My script is hitting the max size/complexity ceiling, but attempts to refactor are running into this issue where libraryScriptA cannot see libraryScriptB (or at least will not compile and so I cant save).
Have been banging my head against a wall - thanks for the tip!

I got bit by this a few days ago.

I have a script that uses the Number.scptd library from @hhas01

I did not realize it, but when it errors it calls another library: TypeSupport.scptd

So, when I installed it on another mac, after gathering and embedding all the libraries I thought it needed (based partly on the SD manifest) I got strange errors (Which hhas helped me figure out).

Would these scripts help in this case?

They should, although Script Debugger 8 tries to do something similar when embedding. Unfortunately neither method is 100% foolproof.

Hello, many thanks for this material and script. I’m left with a question. I have a macOS Service which runs an AppleScript. That script loads a library stored in the /Script Libraries/ folder inside my applet. That library has a use script "DialogToolkitPlus" statement. That worked in the past because I had a copy of DialogToolkitPlus in the ~/Library/Script Library/ folder.

Having run the above script to install symlinks for the applet, I have removed DialogToolkitPlus from the ~/Library/Script Library/ folder. The Service now throws an error:

The action “Run AppleScript” encountered an error: “Can’t get script “DialogToolkitPlus””

I need a way for the script loaded by a Service to see DialogToolkitPlus. I tried creating a /Resources/Script Libraries/ folder inside the Service and adding a use script "DialogToolkitPlus" statement to the Service’s AppleScript. But that resulted in:

The action “Run AppleScript” encountered an error: “The operation couldn’t be completed. (com.apple. Automator error
-212.)”

Is there a way to enable a library loaded by a Service to see the DialogToolkitPlus library ?

Thanks.

Garry

Hello @Garry :wave:

I’m no expert at all but I think it’s better though, if you have a copy of DialogToolkit Plus in ~/Library/Script Libraries or try at least if you could use a Symlink from there to the embedded library inside your Service Bundle.

If it works - then everything is fine. But remember that every single time you’re going to use DialogToolkit Plus again by another applet or script it probably will result in a lot of problems.

What definitely works better is having an Application that has the Library in it and is used to install the Service. But I don’t know anything on how to create such Applications - the only thing to note though is you’ll also have to know how a Service or Quickaction or even Workflow can be created or manipulated using scripting so that it uses the shared Library.

I’ve seen such solutions before, that’s why I know that it can be done.

But again … it’s probably better to have the Library in one of the desired global locations.

Greetings from Germany :de:

Tobias

Hello Tobias,

Thank you for the suggestions. Yes installing in ~/Library/Script Libraries works. However, I’m trying to avoid the need for my users to install DTP. Currently, DTP is in the /Contents/Resources/Script Libraries/ folder in my applet’s bundle. My applet has an installer which copies it to the user’s Library.

So far, I’ve not found a way to enable the script loaded by the Service to see DTP when DTP is inside the applet. One factor in all this is that there is no way to give AppleScript the location of libraries that are referenced by “use script”.

Can you point me to any of those ?

Cheers.

Hello @Garry :wave:

That sounds good… but since you’ve put the library into the ../Contents/Resources/Script Libraries/ Directory inside your Applet - I kinda understand what the issue might be…

The way your installer handles this is that your users will get access to DTP for their own attempts using it. That’s wonderful - but (!!) as far as I know the use script command follows a restricted pattern in terms of searching for the right Libraries or Scripts - which are always based on every type of path that is build upon …/Library/Script Libraries/.

This will allow you (us) to have different locations where we can put our Libraries and the script that tends to use them will always compile. You have an opportunity to use even more than one version if you like by always using different locations when the files are named the same or every version named differently (e.g. [DialogToolKit Plus] version 3, [DialogToolKit Plus] version 3.2, etc.) in the same location as the other versions. However - the use script command will always use the first one it finds based on the pattern I told you about in the previous paragraph. But there are ways to interact with by adding a variable into the command and also telling the code to use a specific version. This way your code again will be compiled and working as desired.

Your Applet comes with Services (Quickactions, Workflows whatever) and your code should be used internally, but also there’s a chance for your users to install the library.

What you need to do is build code that will allow both methods but you’ll need to have DTP in a nested …/Library/Script Libraries/ structure inside your Applet and you‘ll need also to write code that ensures to tell where to find the library. But that’s a little more work.

If your Applet‘s code offers the option to either let only your Applet use the Library or also the user you should also design a second option for the latter where you tell the user that he will be able to access DTP only if your Applet is installed or the other way where the Applet will not be needed to use it.

What you also should be aware of is for the first of these two options: You’ll need to have a LaunchAgent or LaunchService that takes care of this option because it relies on a Symlink pointing from your users Users/[USERNAME]/Library/Script Libraries/ directory to the Library inside the nested …/Library/Script Libraries/ Directory of your Applet. It needs to be there to test for a broken Symlink and the automatic replacement if necessary.

Something more Basics on the run: If you’re using custom directories the aren’t following the rules of the pattern there‘s nothing I am aware of despite using the load script command.

Maybe there are others with more experience than me who can prove me wrong about this. If so then I am happy to learn about this.

I’m sorry, no …

I’ve something on my High Sierra System that is run only and written by a friend of me who sadly passed away a few years ago due to an car accident for my own personal stuff.

I don’t have any documentation on how he did this. He wanted to teach me all his skills but before he was able to even begin with that he died.

He’s written a lot of custom stuff for me and my purpose and I’m really happy that these tools are working without issues but I am struggling to rebuild them for my newer Systems because I am not this brilliant at writing code like he was. And I of course have quite a lot to learn until I am able to do so.

Greetings from Germany :de:

Tobias

Have you tried installing the library script that calls Dialog Toolkit in the app with dialog toolkit? I think applescript looks there first.

Ed, Tobias,

Many thanks for the help.

No, Have thought about it but not tried. I have tried another idea: using a call to osascript to run the target script in background. The call looks like this:

set appPath to [path to the applet]
set myRunThisScriptAsString to quoted form of ((POSIX path of appPath) & "Contents/Resources/Script Libraries/ScriptContainingCode.scptd")
set my_params to quoted form of quoted form of data_for_script
do shell script "osascript -s s " & myRunThisScriptAsString & " " & my_params & " " & " > /dev/null 2> /dev/null &"

The script “ScriptContainingCode.scptd” has an on run(my_params) handler.

I use that approach for another function in the applet and it works. However, in this case I get:

errMSG: sh: line 1: 863 Segmentation fault: 11 osascript …

Don’t know the cause of the error. I’m going to experiment with some prototype osascript calls to try to isolate when the error is triggered. A very simple test with no “use script” command in ScriptContainingCode.scptd works.

In the end, might admit defeat and continue with the old method i.e. always installing DTP in the user’s Library/Script Libraries folder.

Cheers.