One quick tip for easy versioning of your Dart CLI tool
20 Sep 2024
Here's a quick tip that I have used a few times now on various projects and that when I used it again recently on my new static site generator: Picosite I thought it might be worth sharing here to help others in the same situation.
The tip I'm talking about is the need to embed a version number inside your Dart applications stand alone binary. Sure you can just have a const String, just like I initially had in Picosite:
const String version = '0.1.0';
but the problem with this is its not very DRY! We are duplicating the version number whose canonical value is actually stored in our pubspec.yaml
file and when that happens, its easy to forget to update it here and then have them out of sync.
So how can we make use of the version stored in pubspec.yaml
to embed it into the executables produced for end users by our CI? Well normally this is a situation where I would reach for trusty old grep
or awk
and just parse out the number from the pubspec file into an environment variable and from there it would be easy to pass it in via the --define
parameter thats made for exactly this kind of use case in Dart:
VERSION=$(awk '/version:/ {print $2}' pubspec.yml)
dart --define=PS_VERSION=$VERSION compile exe ...
However thats forgetting that Dart can create executables for Linux, MacOS and Windows, BUT only when building on the same platform, so for instance in the GitHub Actions CI workflow for Picosite, I actually need to make use of a matrix and run the compile three separate times, once each on Linux, MacOS and Windows and while the above would be fine on Linux and likely also on MacOS, I'm doubtful about it working on Windows!
So this is where the cross platform nature of Dart and being able to use it for simple "shell style" utility scripts comes to the fore! Because my build of course already depends on Dart, I can write a simple script in Dart to use a regex to pull out the version string out of the pubspec file as with awk
above, but then how to get it into our Dart compile step when we aren't sure that Windows will allow us to use the same Bourne shell style environment variable manipulation as I did with awk
?
The simple solution (and I love simple solutions!) is to just have a string const as we had at the very beginning, but this time in a separate Dart file, say named version.dart
that we import where ever we need to use it and then our simple Dart utility will just overwrite it when its run at the start of the build process. So our Dart utility will be:
import 'dart:io';
void main(List<String> arguments) {
final pubspecFile = File('pubspec.yaml');
if (!pubspecFile.existsSync()) {
print('pubspec.yaml file not found.');
return;
}
final pubspecContent = pubspecFile.readAsStringSync();
final versionRegex = RegExp(r'^version: (\d+\.\d+\.\d+)$', multiLine: true);
final version = versionRegex.firstMatch(pubspecContent);
if (version != null) {
print('Version: ${version.group(1)}');
final output = File("lib/version.dart");
output.writeAsStringSync("const String version = '${version.group(1)}';");
} else {
print('Version not found in pubspec.yaml file.');
}
}
and then in our CI workflow we run it before the compile step:
- run: dart pub get
- run: dart tools/setversion.dart
- run: dart compile exe bin/picosite.dart -v -o output/$
and et voila , we have embedded the version string from pubspec.yaml
into our standalone executable for all three desktop platforms without needing to duplicate it in our code. Of course you will want to have a placeholder version.dart
committed into version control so its there for local development use and what I did is just use an obvious value of 0.0.0
to mark that I'm building debug versions on my development machine, but you can make that string whatever you want.
I hope this has been of help to you and if it has or you have any other handy tips to share, please let me know in the comments below or on Mastodon.