//
// Copyright (c) 2016, Andy Frank
// Licensed under the Apache License version 2.0
//
// History:
// 22 Aug 2016 Andy Frank Creation
//
using util
**
** Assemble project.
**
const class AsmCmd : Cmd
{
override const Str name := "asm"
override const Str sig := "[target]* [--clean]"
override const Str helpShort := "Assemble project"
override const Str? helpFull :=
"By default the asm command will assemble a firmware image for each
target specified in studs.props. If target(s) are listed on the
command line, only these targets will be assembled.
[target]* List of specific targets to assemble, or all if none specified
--clean Delete intermediate system and JRE files
--gen-keys Generate firmware signing keys (fw-key.pub and fw-key.priv)"
** Temp working directory.
const File tempDir := Env.cur.workDir + `studs/temp/`
private Void tempClean() { tempDelete; tempDir.create }
private Void tempDelete() { Proc.run("rm -rf $tempDir.osPath") }
** Firmware signing key files.
private File pubKey() { Env.cur.workDir + `fw-key.pub` }
private File privKey() { Env.cur.workDir + `fw-key.priv` }
override Int run()
{
start := Duration.now
// sanity check
if (Env.cur isnot PathEnv) abort("Not a PathEnv")
// check for --gen-keys
if (opts.contains("gen-keys"))
{
// challenge
if (pubKey.exists || privKey.exists)
{
if (!promptYesNo("Key pair already exists. Regenerate and overwrite? [yN] ", "n"))
abort("cancelled")
}
// regenerate
genKeys
return 0
}
// make sure temp is clean
tempClean
if (opts.contains("clean"))
{
// clean intermediate files
info("Clean")
dirs := File[,]
dirs.addAll((Env.cur.workDir + `studs/systems/`).listDirs)
dirs.addAll((Env.cur.workDir + `studs/jres/`).listDirs)
dirs.each |d|
{
info(" Delete $d.osPath")
d.delete
}
}
else
{
// check for cmdline system filter
System[] systems := args.isEmpty
? props.systems
: args.map |n| { props.system(n) }
// generate keys if not found
if (!pubKey.exists || !privKey.exists) genKeys
// build each target
systems.each |sys|
{
info("Assemble [$sys]")
installSystem(sys)
buildJre(sys)
assemble(sys)
}
// clean up after ourselves
//tempDelete
}
dur := Duration.now - start
loc := (dur.toMillis.toFloat / 1000f).toLocale("0.00")
info("ASM SUCCESS [${loc}sec]!")
return 0
}
** Generate firmware signing keys.
Void genKeys()
{
baseDir := Env.cur.workDir
// start fresh
pubKey.delete
privKey.delete
// gen keys
info("Generate firmware signing keys...")
Proc.bash("cd $baseDir.osPath; fwup --gen-keys")
Proc.run("mv $baseDir.osPath/fwup-key.pub $pubKey.osPath")
Proc.run("mv $baseDir.osPath/fwup-key.priv $privKey.osPath")
info("Keep your private key in a safe location!")
}
** Download and install the system configuration for target.
Void installSystem(System sys)
{
baseDir := Env.cur.workDir + `studs/systems/`
baseDir.create
sysDir := baseDir + `$sys.name/`
// check if up-to-date
sysProps := sysDir + `system.props`
if (sysProps.exists)
{
// bail here if up-to-date
ver := Version(sysProps.readProps["version"] ?: "", false)
if (sys.version == ver) return
// prompt to upgrade
// TODO: do we abort if out-of-date???
if (!promptYesNo("Upgrade $sys.name $ver -> $sys.version? [Yn] ")) return
Proc.run("rm -rf $sysDir.osPath")
}
tar := baseDir + `$sys.uri.name`
if (sys.uri.scheme == "http" || sys.uri.scheme == "https")
{
// download tar
Proc.download(" Downloading $sys system", sys.uri, tar)
}
else
{
// assume uri is a local file
tar = sys.uri.toFile
if (!tar.exists) abort("file not found: $tar.osPath")
}
// untar
info(" Install $sys system...")
Proc.run("tar xvf $tar.osPath -C $baseDir.osPath")
// cleanup (if downloaded)
if (sys.uri.scheme == "http" || sys.uri.scheme == "https") tar.delete
}
** Build compact JRE for target.
Void buildJre(System sys)
{
profDir := profile["jres.dir"]?.toUri?.toFile
baseDir := Env.cur.workDir + `studs/jres/`
baseDir.create
jreDir := baseDir + `$sys.jre/`
// determine JRE compact profile to use
jreProfStr := props["jre.profile"] ?: "1"
jreProfile := jreProfStr.toInt(10, false)
if (jreProfile == null || !(1..3).contains(jreProfile))
Proc.abort("invalid jre.profile '$jreProfStr'")
// TODO: check if profile has changed?
// bail if already exists
if (jreDir.exists) return
// TODO: we need to check for latest version... but does that go
// away once we starting building our own JRE with OpenJDK 9?
// find source tar image - first check local dir, and if not
// found try to find the profile dir if one is defined
find := |File dir->File?| {
dir.listFiles.find |f| { f.name.endsWith("${sys.jre}.tar.gz") }
}
tar := find(baseDir)
if (tar == null && profDir != null) tar = find(profDir)
if (tar == null) Proc.abort("no jre found for $sys.name")
// unpack
tempClean
info(" Build ${jreDir.name} jre [compact${jreProfile}]...")
Proc.run("tar xf $tar.osPath -C $tempDir.osPath")
// invoke jrecreate (requires Java 7+)
javaHome := Env.cur.vars["java.home"]
jdkDir := tempDir.listDirs.find |d| { d.name.startsWith("ejdk") }
Proc.bash(
"export JAVA_HOME=$javaHome
${jdkDir.osPath}/bin/jrecreate.sh --dest $jreDir.osPath --profile compact${jreProfile} -vm client")
}
** Assemble firmware image for target.
Void assemble(System sys)
{
// dir setup
jreDir := Env.cur.workDir + `studs/jres/$sys.jre/`
sysDir := Env.cur.workDir + `studs/systems/$sys.name/`
relDir := Env.cur.workDir + `studs/releases/`
relDir.create
tempClean
rootfs := tempDir + `rootfs_overlay/`
rootfs.create
// release image name
proj := props["proj.name"]; if (proj==null) abort("missing 'proj.meta' in studs.props")
ver := props["proj.ver"]; if (ver==null) abort("missing 'proj.ver' in studs.props")
urel := relDir + `${proj}-${ver}-${sys.name}._fw`
srel := relDir + `${proj}-${ver}-${sys.name}.fw`
// defaults
fwupConf := sysDir + `images/fwup.conf`
// stage jre
info(" Stage rootfs...")
(rootfs + `app/`).create
Proc.run("cp -R $jreDir.osPath $rootfs.osPath/app")
Proc.run("mv $rootfs.osPath/app/${sys.jre} $rootfs.osPath/app/jre")
// stage faninit
init := Pod.find("studsTools").file(`/bins/$sys.name/faninit`)
init.copyTo(rootfs + `sbin/init`)
Proc.run("chmod +x $rootfs.osPath/sbin/init")
// faninit.props
initProps := Env.cur.workDir + `faninit.props`
initProps.copyTo(rootfs + `etc/faninit.props`)
initProps.readProps.keys.each |n|
{
if (faninitRetired.containsKey(n))
info(" # [faninit.props] '$n' prop not longer used")
}
// sys.props
sysProps := Str:Str[:] {
it.ordered = true
it.set("proj.name", proj)
it.set("proj.version", ver)
it.set("studs.version", AsmCmd#.pod.version.toStr)
it.set("system.name", sys.name)
it.set("system.version", sys.version.toStr)
}
(rootfs + `etc/sys.props`).writeProps(sysProps)
// fw-key.pub
pubKey.copyTo(rootfs + `etc/fw-key.pub`)
// stage scripts
["udhcpc.script"].each |name|
{
script := Pod.find("studsTools").file(`/scripts/$name`)
script.copyTo(rootfs + `usr/bin/$name`)
Proc.run("chmod +x $rootfs.osPath/usr/bin/$name")
}
// stage natives
["fangpio", "fani2c", "fannet", "fanspi", "fanuart"].each |name|
{
bin := Pod.find("studsTools").file(`/bins/$sys.name/$name`)
bin.copyTo(rootfs + `usr/bin/$name`)
Proc.run("chmod +x $rootfs.osPath/usr/bin/$name")
}
// stage libfan
libfan := Pod.find("studsTools").file(`/bins/$sys.name/libfan.so`)
libfan.copyTo(rootfs + `usr/lib/libfan.so`)
// stage app
podWhitelist := props["pod.whitelist"]?.split(',') ?: Str#.emptyList
podBlacklist := props["pod.blacklist"]?.split(',') ?: Str#.emptyList
(rootfs + `app/fan/lib/fan/`).create
(rootfs + `app/fan/lib/java/`).create
(Env.cur.homeDir + `lib/java/sys.jar`).copyTo(rootfs + `app/fan/lib/java/sys.jar`)
(Env.cur as PathEnv).path.each |path|
{
pods := (path + `lib/fan/`).listFiles.findAll |f|
{
if (f.ext != "pod") return false
if (podWhitelist.contains(f.basename)) return true
if (podBlacklist.contains(f.basename)) return false
if (podDefBlacklist.contains(f.basename)) return false
return true
}
pods.each |p| { p.copyTo(rootfs + `app/fan/lib/fan/$p.name`) }
}
// unit database
units := Env.cur.homeDir + `etc/sys/units.txt`
units.copyTo(rootfs + `app/fan/etc/sys/$units.name`)
// tz database
tzData := Env.cur.homeDir + `etc/sys/timezones.ftz`
tzAlias := Env.cur.homeDir + `etc/sys/timezone-aliases.props`
tzJs := Env.cur.homeDir + `etc/sys/tz.js`
tzData.copyTo(rootfs + `app/fan/etc/sys/$tzData.name`)
tzAlias.copyTo(rootfs + `app/fan/etc/sys/$tzAlias.name`)
tzJs.copyTo(rootfs + `app/fan/etc/sys/$tzJs.name`)
// copy user rootfs_overlay
userRootfs := Env.cur.workDir + `src/rootfs_overlay/${sys.name}/`
if (userRootfs.exists) Proc.run("cp -Rf $userRootfs.osPath/ $rootfs.osPath")
// stage data
(rootfs + `data/`).create
// merge rootfs
info(" Merge rootfs...")
Proc.run(
"$sysDir.osPath/scripts/merge-squashfs " +
"$sysDir.osPath/images/rootfs.squashfs " +
"$tempDir.osPath/combined.squashfs " +
"$tempDir.osPath/rootfs_overlay")
// assemble image
info(" Assemble firmware image...")
Proc.bash(
"export NERVES_SYSTEM=$sysDir.osPath
export ROOTFS=$tempDir.osPath/combined.squashfs
fwup -c -f $fwupConf.osPath -o $urel.osPath")
// sign release image
info(" Signing firmware image...")
Proc.run("fwup -S -s $privKey.osPath -i $urel.osPath -o $srel.osPath")
Proc.run("rm $urel.osPath")
// indicate image filepath
size := srel.size.toLocale("B")
info(" Release:")
info(" $srel.osPath [$size]")
}
** List of retired faninit prop names
static const Str:Str faninitRetired := [:].setList([
"fs.mount",
])
** Blacklist of pods to remove from app staging.
static const Str[] podDefBlacklist := [
"studsTest",
"studsTools",
"build", // build tools
"compiler",
"compilerDoc",
"compilerJava",
"compilerJs",
"docDomkit", // docs
"docFanr",
"docIntro",
"docLang",
"docTools",
"icons", // fwt and flux
"gfx",
"fwt",
"webfwt",
"flux",
"fluxTest",
"syntax",
"email", // misc
"fandoc",
"fanr",
"fansh",
"obix",
"sql",
"testCompiler", // unit tests
"testDomkit",
"testJava",
"testNative",
"testSys",
]
}