Build Tag

This example shows how to tag a build with info from when it was built using code generation. Here the version number, git hash, timestamp, and user name are put into the generated .java file so it's an accurate reflection of when the code was last generated. There are several features demonstrated:

  • How to define a property in a layer for use by subsequent layers (scmVersion).
  • Running shell commands in a layer definition file.
  • Using the @BuildInit annotation to initialize a runtime property to the value of a build-time expression. The build-time expressions are run in the context of the buildLayer using the dynamic runtime so they can refer to layer components, or layer fields in any layer in the current build stack but not the class being generated. It can also use the "layeredSystem" property or any static methods available in the dynamic runtime. If there are errors in the build time expression, those will be visible at compile time (TODO: support edit time resolution here like we do for schtml attributes?).
  • How to initialize a runtime property from layered .properties files
Here's the buildTag layer definition file, run at build time:
file: example/buildTag/buildTag.sc
package sc.example.buildTag;

/** 
   The code here is interpreted, not compiled as it's the layer definition so it runs at build time. 
   When the layer is initialized, we'll run some git commands to create an scmVersion which can then
   be used in a BuildInit annotation (see ExBuildTag.sc) 
 */
public example.buildTag extends sys.std {
   String scmVersion;

   void init() {
      if (!activated) // Not loaded as part of a build so nothing to do here.
         return;
      String branch = FileUtil.execCmd("git symbolic-ref --short HEAD"); // Current branch name only
      if (branch != null) {
         branch = branch.trim();
         String hash = FileUtil.execCmd("git rev-parse --short HEAD").trim();

         // Look at 'git status' and see if there are local changes in this build - if so, summarize them like '3M,2C,1D'
         String status = FileUtil.execCmd("git status -suno --porcelain").trim();
         String repoStatus = ""; 
         if (status.length() > 0) {
            String[] statusLines = status.split("\n");
            int numMods = 0, numAdds = 0, numDels = 0, numOther = 0;
            for (String line:statusLines) {
               char start = line.trim().charAt(0);
               switch (start) {
                  case 'M':
                     numMods++;
                     break;
                  case 'A':
                     numAdds++;
                     break;
                  case 'D':
                     numDels++;
                     break;
                  default:
                     numOther++;
                     break;
               }
            }
            if (numMods != 0) 
               repoStatus += numMods + "M";
            if (numAdds != 0)
               repoStatus += repoStatus.length() == 0 ? "" : "," + numAdds + "A";
            if (numDels != 0)
               repoStatus += repoStatus.length() == 0 ? "" : "," + numDels + "D";
            if (numOther != 0)
               repoStatus += repoStatus.length() == 0 ? "" : "," + numOther + "?";
            repoStatus = "+" + repoStatus;
         }
         scmVersion = branch + '@' + hash + repoStatus; 
      }
      else 
         scmVersion = "not from git dir";
   }
}
It defines the layer field 'scmVersion' which is initialized by running git and parsing the result to produce an informative string that includes information about local changes in the repository at the time of the build. The 'init' method is run just after the layer is constructed, before the start and validate methods.

Here's the source for the BuildTag subclass that is generated to include the build info:

file: example/buildTag/ExBuildTag.sc
import sc.obj.BuildInit;
import sc.layer.LayerUtil;

/** 
  * This file is used to inject values from build-time expressions into the generated code. 
  * We can take the binaries with specific info derived from when it was built. 
  */
object ExBuildTag extends sc.util.BuildTag {
   override @BuildInit("new java.util.Date().toString()") timeStamp;
   override @BuildInit("System.getProperty(\"user.name\")") user;
   override @BuildInit("System.getProperty(\"java.version\")") javaVersion;
   // Want one string for the osVersion.  TODO: maybe call this osVersionStamp?
   override @BuildInit("System.getProperty(\"os.name\") + '.' + System.getProperty(\"os.arch\") + '.' + System.getProperty(\"os.version\")") osVersion;
   // Returns the top-most 'build.properties' definition of a property or null if there is none
   override @BuildInit("getLayerProperty(\"build\",\"version\")") version;
   override @BuildInit("getLayerProperty(\"build\",\"tag\")") tag;
   override @BuildInit("getLayerProperty(\"build\",\"revision\")") revision;
   override @BuildInit("sc.layer.LayerUtil.incrBuildNumber(\"scc\")") buildNumber;
   // Evaluates a layer variable scmVersion
   override @BuildInit("scmVersion") scmVersion;
}
  • The @BuildInit annotation is a bridge between 'build time' and 'run time'. It's used to initialize runtime properties to build time expressions.
  • The call to getLayerProperty looks for a build.properties in the top-level layer directory, merging those in subsequent layers. Layer properties can be overridden using the -P option to scc that works like Java's -D. Use: 'scc -Pbuild.tag=release', and 'getLayerProperty("build", "tag")'.
example/buildTag/build.properties
version=0.1.0
tag=dev

To compile a release build, you can override the defaults by adding a command line option: scc -Pbuild.tag=release example.buildTag