How to use the AppBuilder
script to create a minimal Fantom installation that runs your application.
Let's say you've created a cool Fantom application, like the Gundam Game, and you want to share it with your mates.
But installing it on your mates' computers can be quite a hassle:
- First they needs Fantom installed
- Then they needs a copy of your
.pod
library. - Then they needs a copy of all the
.pods
your.pod
depends on (whose versions may conflict with those they're already using) - They may also need
.jar
files, likeswt.jar
, specific to their computers' platforms - They may also need specific files from
%FAN_HOME%/etc/ *
folders or elsewhere
And all you want is for them to play your game!
Wouldn't it great if instead, you could just hand them a .zip
file that contained everything they needed? A .zip
file with a simple script file that launched the application?
Well, you can!
Enter AppBuilder.fan!
This simple Fantom script let's you do just that!
It creates a minimal Fantom installation with only those pods and files required to run your application. Meaning your application can be run in isolation to any other Fantom installation. No missing pods, no version conflicts, no missing files, just your app - running happily!
All your application needs to run, is java
available on the command line.
AppBuilder
is a simple class that copies application files into a build directory and zips them up.
A minimal build script looks like:
AppBuilder("<pod-name>").build()
Where <pod-name>
is the name of your application's pod.
To use, both the Main
program that runs the script, and the AppBuilder
class have to be in the same file.
Therefore a sample build script for flux would look like:
class Main { static Void main() { AppBuilder("flux") { it.jarFiles = ["swt.jar"] }.build() } } const class AppBuilder { ... ... ... }
The file may be called anything you like, but I tend to use AppBuilder.fan
. Then to run it, I type:
C:\> fan AppBuilder.fan Packaging Fantom Core 1.0.67 ============================ Copying Java runtime... - copied lib\java\sys.jar Copying application pods... - copied lib\fan\flux.pod - copied lib\fan\sys.pod - copied lib\fan\concurrent.pod - copied lib\fan\gfx.pod - copied lib\fan\fwt.pod - copied lib\fan\compiler.pod - copied etc\flux - copied etc\sys Copying jar files... - copied lib\java\ext\linux-x86\swt.jar - copied lib\java\ext\macosx-x86_64\swt.jar - copied lib\java\ext\win32-x86\swt.jar - copied lib\java\ext\win32-x86_64\swt.jar Creating script files... - copied fanlaunch - copied flux - copied flux.bat Calling user function... Compressing to flux-1.0.67.zip ... - C:\Temp\build\flux-1.0.67.zip Done.
The Default Build
The default build process does the following...
- Creates a build directory
- Copies Fantom's
sys.jar
- Copies your application's pod and any dependent pods
- Copies any specified jar files for desired platforms
- Creates a simple script file to launch the app
- Compresses the build directory to a
.zip
file
All files are copied from your existing Fantom installation. Some people customise their Fantom environment by specifying the FAN_ENV
environment variable (usually to util::PathEnv
), and AppBuilder
takes this into account. But by setting AppBuilder.useEnv
to false
then all files are resolved relative to the %FAN_HOME%
directory.
When a pod is copied, then all the pods it depends on copied over too. And all the pods they depend on, and so on... Also, the contents of any related <FANTOM_HOME>/etc/*
directory is copied over too - both for the named pod, and any dependent pod.
When a .jar
file is copied, it is looked for in both the <FANTOM-HOME>/lib/java/
folder and in any nested platform folder. This lets you create a minimal installation that targets a specific platform such as Windows 64 bit.
Basic script files are created that call java
and launch Fantom, passing in any specified script arguments. The script arguments default to the name of the pod.
All the above can be customised by changing the script's field values in the AppBuilders
ctor. Fields that may be customised are:
File buildDir
Bool useEnv
File fantomHomeDir
Str[] excludePods
Str[] jarFiles
Str[] platforms
Str scriptArgs
For more details on the specific fields, read the API comments in the script source.
Customising the Build
The AppBuilder.build()
method takes an optional func parameter that lets you perform extra build steps just before the application directory is zipped up. The function takes an AppBuilder
parameter which makes available several useful methods:
findFile()
copyFile()
copyPod()
copyJarFiles()
createScriptFiles()
compressToZip()
All the above methods are used by the main build script itself. Read the API comments in the script source for further information.
AppBuilder.fan
The AppBuilder
script is available as a BitBucket Snippet and is also given below:
class Main { static Void main() { AppBuilder("myPodName") {// it.jarFiles = ["swt.jar"]// it.platforms = ["win32-x86_64"]// it.scriptArgs = "${it.podName} -cheatMode on"}.build |bob| {// bob.copyFile(findFile(`etc/myapp/hiscores.txt`), `etc/myapp/`)} } }// ---- Do not edit below this line ---------------------------------------------------------------** Fantom App Builder v0.0.4** =========================** Creates a standalone Fantom application installation with the minimum number of files.**** v0.0.4 - Added 'findPodFile()' to make work with Fantom Pod Manager.** v0.0.2 - Initial release.**const class AppBuilder {** The name of the main application pod.const Str podName** The directory where the application is assembled.** Defaults to 'build/'const File buildDir** If 'true' (the default), then files (pods and libraries) are located using the current 'Env'.** If 'false', files are taken to be relative to the 'fantomHomeDir'.**** See 'findFile()' method.const Bool useEnv := true** The Home directory of the Fantom installation.** If 'useEnv' is 'false' then all pods and libraries are taken from this location.** Defaults to `sys::Env.homeDir`.**** fantomHomeDir = File.os("C:\\Apps\\fantom-1.0.68\\")const File fantomHomeDir := Env.cur.homeDir** Names of pods that should not be included in the distribution.**** excludePods = ["compiler"]const Str[] excludePods := Str[,]** A list of java libraries (jar file names) that are to be copied over.** Jar files are copied from 'lib/java/ext/' and 'lib/java/ext/XXXX/' where 'XXXX' is a matching platform name.** See 'platforms' field for details.**** jarFiles = ["swt.jar"]const Str[] jarFiles := Str[,]** A list of platforms (regex globs) that '.jar' files will be copied over for. Use to target specific platforms:**** platforms = ["win32-x86*"] // is the same as** platforms = ["win32-x86", "win32-x86_64"]**** Defaults to 'Str["*"]' which targets *all* platforms.const Str[] platforms := Str["*"]** Arguments to pass to the fantom launch scripts.**** Defaults to 'podName' to launch the application.const Str scriptArgs private const File _distDir** Creates an instance of 'AppBuilder' for the given pod.new make(Str podName, |This|? in := null) { this.podName = podName this.buildDir = File(typeof->sourceFile.toStr.toUri).normalize.parent + `build/` in?.call(this) if (!buildDir.isDir) throw ArgErr("buildDir is NOT a directory - ${buildDir.normalize.osPath}") if (!fantomHomeDir.isDir) throw ArgErr("fantomHomeDir is NOT a directory - ${fantomHomeDir.normalize.osPath}") this.buildDir = this.buildDir.normalize this._distDir = this.buildDir + `${podName}-${Pod.find(podName).version}/` if (scriptArgs == null) scriptArgs = podName }** Builds the packaged installation.** 'extra' is called before the .zip file is created to allow you to perform any extra tasks;** such as copying over surplus files.Void build(|AppBuilder|? extra := null) { pod := Pod.find(podName) name := (pod.meta["proj.name"] ?: podName) + " ${pod.version}" log log("Packaging ${name}") log("".padl(name.size+10, '='))// cleanif (_distDir.exists) { log("\nDeleting `${_distDir.osPath}`...") _distDir.delete } _distDir.create// copy java runtimelog("\nCopying Java runtime...") copyFile(findFile(`lib/java/sys.jar`), `lib/java/`)// copy podslog("\nCopying application pods...") copyPod(podName)// copy jarsif (jarFiles.size > 0) { log("\nCopying jar files...") copyJarFiles(jarFiles, platforms) } log("\nCreating script files...") createScriptFiles(podName, scriptArgs) if (extra != null) { log("\nCalling user function...") extra.call(this) } zipName := `${podName}-${Pod.find(podName).version}.zip` log("\nCompressing to ${zipName} ...") zipFile := compressToZip(_distDir, zipName) log(" - ${zipFile.osPath}") _distDir.delete log("\nDone.") }** Copies over jar file from the existing Fantom environment.** The parameters have the same meaning as `jarFiles` and `platforms`.Void copyJarFiles(Str[] jarFiles, Str[] platformGlobs) { jarFiles.each |jarFileName| { copyFile(findFile(`lib/java/ext/${jarFileName}`, false), `lib/java/ext/`) extDir := findFile(`lib/java/ext/`, false) if (extDir != null) { platformGlobs.each |platform| { extDir.listDirs(Regex.glob(platform)).each |libDir| { copyFile(findFile(`lib/java/ext/${libDir.name}/${jarFileName}`, false), `lib/java/ext/${libDir.name}/`) } } } } }** Copies over the named pod, along with all (transitive) dependencies and any associated 'etc/' property directory.Void copyPod(Str podName, Bool copyEtcFiles := true, Bool copyDependencies := true) { podNames := copyDependencies ? _findPodDependencies(Str[,], podName).unique : [podName] podNames.unique.each |pod| {// log versions of non-core podsver := Pod.find(pod).version == Pod.find("sys").version ? "" : " (v${Pod.find(pod).version})" _copyFile(findPodFile(pod, true), `lib/fan/${pod}.pod`, false, ver) } if (copyEtcFiles) podNames.unique.each |pod| { copyFile(findFile(`etc/${pod}/`, false), `etc/${pod}/`) } }** Copies the given file to the destination URL - which is relative to the output folder.** Returns the destination file, or null if 'srcFile' is 'null' or does not exist.**** copyFile(`fan://acme/res/config.props`.get, `etc/config.props`)**** If 'srcFile' is a dir, then the entire directory tree is copied over.**** If 'destUrl' is a dir, then the file is copied into it.File? copyFile(File? srcFile, Uri destUri, Bool overwrite := false) { _copyFile(srcFile, destUri, overwrite, Str.defVal) }** Compresses the given file to a .zip file at the destination URL- which is relative to the output folder.** Returns the compressed .zip file.**** 'toCompress' may be a directory.File compressToZip(File toCompress, Uri destUri) { if (destUri.isDir) throw ArgErr("Destination can not be a directory - ${destUri}") if (!destUri.isPathOnly) throw ArgErr(_msg_urlMustBePathOnly("destUri", destUri, `my-app.zip`)) if (destUri.isPathAbs) throw ArgErr(_msg_urlMustNotStartWithSlash("destUri", destUri, `my-app.zip`)) bufferSize := 16*1024 dstFile := (buildDir + destUri).normalize zip := Zip.write(dstFile.out(false, bufferSize))// DO include the name of the containing folder in zip pathsparentUri := toCompress.parent.uri// do NOT include the name of the containing folder in zip paths//parentUri := toCompress.isDir ? toCompress.uri : toCompress.parent.uritry { toCompress.walk |src| { if (src.isDir) return path := src.uri.relTo(parentUri) out := zip.writeNext(path) try { src.in(bufferSize).pipe(out) } finally out.close } } finally zip.close return dstFile }** Creates basic script files to launch the application.Void createScriptFiles(Str baseFileName, Str scriptArgs) { copyFile(findFile(`bin/fanlaunch`, true), `fanlaunch`) bshScript := "#!/bin/bash\n\nexport FAN_HOME=.\nunset FAN_ENV\n. \"\${0%/*}/fanlaunch\"\nfanlaunch Fan ${scriptArgs} \"\$@\"" bshFile := (_distDir + `${baseFileName}`).normalize.out.writeChars(bshScript).close log(" - copied ${baseFileName}") cmdScript := "@set FAN_HOME=.\n@set FAN_ENV=\n@java -cp \"%FAN_HOME%\\lib\\java\\sys.jar\" fanx.tools.Fan ${scriptArgs} %*" cmdFile := (_distDir + `${baseFileName}.bat`).normalize.out.writeChars(cmdScript).close log(" - copied ${baseFileName}.bat") }** Resolves a file based on the given relative URI.** If 'useEnv' is 'true' then 'Env.cur.findFile(...)' is used to find the file, otherwise it is** taken to be relative to 'fantomHomeDir'.File? findPodFile(Str podName, Bool checked := true) { if (useEnv) return Env.cur.findPodFile(podName) ?: (checked ? throw ArgErr("Could not find pod file for ${podName}") : null) file := (fantomHomeDir + `lib/fan/${podName}.pod`).normalize return file.exists ? file : (checked ? throw ArgErr("File not found - ${file}") : null) }** Resolves a pod file based on its name.** If 'useEnv' is 'true' then 'Env.cur.findFile(...)' is used to find the file, otherwise it is** taken to be relative to 'fantomHomeDir'.File? findFile(Uri fileUri, Bool checked := true) { if (useEnv) return Env.cur.findFile(fileUri, checked) if (fileUri.isPathAbs) throw ArgErr(_msg_urlMustNotStartWithSlash("fileUri", fileUri, `etc/config.props`)) file := (fantomHomeDir + fileUri).normalize return file.exists ? file : (checked ? throw ArgErr("File not found - ${file}") : null) }** Echos the msg.static Void log(Obj? msg := "") { echo(msg?.toStr ?: "null") } private File? _copyFile(File? srcFile, Uri destUri, Bool overwrite, Str append) { if (!destUri.isPathOnly) throw ArgErr(_msg_urlMustBePathOnly("destUri", destUri, `etc/config.props`)) if (destUri.isPathAbs) throw ArgErr(_msg_urlMustNotStartWithSlash("destUri", destUri, `etc/config.props`)) if (srcFile == null) return null if (!srcFile.exists) { log("Src file does not exist: ${srcFile?.normalize?.osPath}") return null } if (destUri.isDir && !srcFile.isDir) destUri = destUri.plusName(srcFile.name) dstFile := (_distDir + destUri).normalize srcFile.copyTo(dstFile, ["overwrite": overwrite]) log(" - copied " + dstFile.uri.relTo(_distDir.uri).toFile.osPath + append) return dstFile } private Str[] _findPodDependencies(Str[] podNames, Str podName) { if (!excludePods.contains(podName) && !podNames.contains(podName)) { podNames.add(podName) pod := Pod.find(podName) pod.depends.each |depend| { _findPodDependencies(podNames, depend.name) } } return podNames } private static Str _msg_urlMustBePathOnly(Str type, Uri url, Uri example) { "${type} URL `${url}` must ONLY be a path. e.g. `${example}`" } private static Str _msg_urlMustNotStartWithSlash(Str type, Uri url, Uri example) { "${type} URL `${url}` must NOT start with a slash. e.g. `${example}`" } }
Have fun!