Blog tagged runtime

Ceylon Bootstrap

Ceylon 1.2.2 was released back in March, and at the time it shipped with a new feature that did not make a lot of noise, but I think it's worth explaining it a bit more in this blog.

In general Ceylon developers have to download the Ceylon distribution in order to compile Ceylon code, and so do Ceylon users. When you check out a Ceylon project you have to make sure you're using the right version of the Ceylon distribution to compile it, and switching Ceylon distribution can be a bit too involved on some Operating Systems.

To solve that issue, Tako has implemented the ceylon bootstrap tool, very heavily inspired by the Gradle Wrapper, which allows you to generate a small script in your Ceylon project which your users and developpers can use to automatically download the right Ceylon distribution and cache it for future use.

Let's try it out by generating a new Ceylon project (on a machine with a Ceylon 1.2.2 distribution, this is a chicken and egg problem and we have to start somewhere):

$ ceylon new hello-world --ant=no --eclipse=no myproject
Enter module name [com.example.helloworld]: 
Enter module version [1.0.0]:

Now let's check that it compiles and runs:

$ cd myproject
$ ceylon compile,run com.example.helloworld
Note: Created module com.example.helloworld/1.0.0
Hello, World!

Excellent! Now let's add the Ceylon bootstrap tool:

$ ceylon bootstrap
$ ls ceylonb*
ceylonb  ceylonb.bat
$ ls .ceylon/bootstrap/
ceylon-bootstrap.jar  ceylon-bootstrap.properties

As you can see, we've created two ceylonb scripts (one for Unix, one for Windows), and a new configuration folder for those scripts containing a tiny jar (24k) and a settings file which points to the distribution to use:

$ cat .ceylon/bootstrap/ceylon-bootstrap.properties 
#Generated by 'ceylon bootstrap'
#Tue Aug 02 17:29:39 CEST 2016
distribution=https\://downloads.ceylon-lang.org/cli/ceylon-1.2.2.zip

These scripts and folders should be committed to your project code, so they can be used by others.

Now, whenever someone checks your project out, they can just use the provided ceylonb script to work rather than pre-install the Ceylon distribution:

$ ./ceylonb compile,run com.example.helloworld
Downloading Ceylon... 100%
Note: Created module com.example.helloworld/1.0.0
Hello, World!

The first time you run this, it will download the desired Ceylon distribution to ~/.ceylon/dists, but from then on, whenever you invoke ceylonb it will reuse that distribution without downloading anything. Even other projects using the same distribution will be able to use it.

As I said, Gradle Wrapper users are used to this, but still, it's a really good idea and there's no reason why programming languages couldn't do the same!

Modularity Changes

TL;DR: This article describes the modularity changes in the Ceylon run-time and distribution, in order to make them lighter at run-time. Skip to the Final runtime dependencies section if you just want the outcome.

As we initially expected most Ceylon users to run their code using the ceylon run command, we figured that since they have the Ceylon distribution installed, it does not matter if they depend on more modules from that distribution than strictly necessary. Those modules had to be there anyway, so it would not save any bandwidth to reduce those dependencies.

Naturally, we were wrong, and between the Ceylon Eclipse or IntelliJ IDEs, running Ceylon on OpenShift, WildFly, Vert.x or on Android, people started running Ceylon without the distribution installed, using just the standard java runner. It became soon apparent that we had to untangle those dependencies to make the runtime requirements lighter.

Historically we had the following modules:

  • common module used by other modules
  • typechecker (the shared compiler front-end)
  • Java compiler back-end
  • JavaScript compiler back-end
  • module repository system
  • JBoss modules runtime

The model module

When we implemented reified generics, we had to add subtyping to the runtime, so that we'd be able to figure out if is T bar was true or not. The easiest thing at the time was to "just" depend on the typechecker (compiler front-end) which dealt with the language model and subtyping, and the Java compiler back-end, which had infrastructure to load a language model from JVM information such as class files, or in this case reflection.

This essentially made the runtime depend on the compiler front-end and back-end, which we realised was not ideal, so during the Ceylon 1.2 development, we extracted all model description, loading and subtyping to a new ceylon-model module, but we did not have enough time to do more and so these dependencies remained due to other causes.

Supporting Java 9

During our work on supporting Java 9 / Jigsaw modules in Ceylon, it became clear that having kept our "fork" of javalang tools (that we use for javac) under its original package name would not work anymore, we renamed its package and used the opportunity to prune away parts of the java tools we did not use. We also extracted the class-file reader part to its own module so we could use it outside of the compiler to remove our dependency to jandex (a class-file scanner).

Finally, when we created the ceylon jigsaw tool (which populates a folder with the jar files required by a Ceylon module, to run it on a Java 9 VM) it became evident that the runtime still depended not just on the compiler front-end and Java back-end, but even on the JavaScript back-end, which frankly made little sense in most JVM executions.

These dependencies were due to the Ceylon Tool Provider API having snuck into the ceylon.language module as a convenience (at the time). Since that allowed you to compile and run Ceylon programmatically for both Java and JavaScript back-ends, it had to depend on the tools.

We decided to split the Ceylon Tool Provider into its own module and got rid of the final dependencies from the language module to the compilers and typechecker, but had no more time to get rid of further dependencies such as JBoss Modules and Aether in time for Ceylon 1.2.2.

Supporting Android

Initial work on running Ceylon on Android revealed that what passes for small dependencies on ordinary JVM executions, or even on Java EE deployments, was not an option on Android where every method counts.

At this point we had to bite the bullet and make every non-required transitive dependency go.

We noticed that the old common module had grew to include the Command-Line Tooling API that makes the ceylon command and its subcommands and plugins work. That in turn depended on a Markdown renderer used by ceylon doc. It was pretty trivial to extract it to its own module because this was never used in Ceylon user programs.

Next in line was our Shrinkwrap Resolver dependency, which our module repository system uses to interoperate with Maven repositories. This was a fat-jar with all its dependencies included, including some Apache Commons modules, and an outdated version of Eclipse Aether. That fat-jar had already been problematic in our Maven module, which already had its version of Aether, so getting rid of the fat-jar was a good idea. We also realised that some of its Apache Commons dependencies were already included outside the fat-jar in our distribution repository, so there was that duplication to fix too.

So what we did was remove the Shrinkwrap Resolver dependency and use Aether directly, by incorporating all its subcomponents into our distribution. It turns out that because the latest version of Aether requires Google Guava, our distribution grew in size rather than shrink (that jar is huge). But to offset that, we made the Aether dependency optional, and made sure it was possible to run Ceylon without it as long as there was some compilation step beforehand that provided all the Maven dependencies that you may use in interop. ceylon fat-jar or ceylon jigsaw would do that for you, for example.

Our module repository system also provided support for writing to WebDAV or Herd repositories, which required some dependencies on Apache Http Client or Sardine, and we made these dependencies optional as well, because at runtime your Ceylon program is very unlikely to write to HTTP repositories. This is something only the compiler and other tools do.

We also removed a dependency to JBoss Modules from the language module using abstraction, since that platform was optional and never used on Android or other flat-classpath runtimes.

Finally, the language module only had one dependency left on the (much slimmer) module repository system via the presence of the Main API in there, and we moved that class to its own module.

Final runtime dependencies

After all this pruning, the language module on the JVM is back down to requiring the following set of transitive dependencies:

  • common (small and free of tooling and dependencies)
  • model (which depends only on the class-file reader)
  • class-file reader

So your Ceylon module will only depend on four jars (these three and the language module), the sum size of which is 2.4 Mb, which is much smaller than initially, and has dramatically less methods, at around 17148 methods. This is still too much, but can be brought down by tooling such as ProGuard to remove unused classes. Remember this includes a runtime for an entire language, so it's not that big, all things considered.

SDK changes

In order to be able to use Ceylon's HTTP client on Android, we also split up the ceylon.net module from the Ceylon SDK into client and server modules. Otherwise the HTTP server and its dependencies were too much drag for Android's method count.

Ceylon Fat Jars

Ceylon 1.2.3 is looming closer, and so we should start talking about some of the new features that it will contain. One of those new features is the new ceylon fat-jar command.

ceylon fat-jar my.module/1 lets you generate a jar which contains the my.module/1 module and every required dependency. Once you have that jar, you can execute it with java -jar my.module-1.jar and it will execute the module.

Naturally, you can customise the function or class to execute with the --run option, but the default is the run method in your module (my.module::run in our case).

The modules are executed in a "flat classpath" mode, without JBoss Modules or any sort of module isolation, and with the metamodel already set up statically when the fat jar was created, so it's a little bit different to the older alternative of running Ceylon modules using the Main API and ceylon classpath tool, which did not deal with packing all the dependencies. It is also different to the ceylon run --flat-classpath in that it does not require a module repository at run-time, since ceylon fat-jar packs it all together.

Try it out, it's pretty useful to distribute Ceylon programs and run them in places without having to install the Ceylon distribution.