GymPal/build.d

209 lines
5.6 KiB
D
Executable File

#!/usr/bin/env rdmd
/**
* This module is responsible for building the project.
*/
module build;
import std.stdio;
import std.file;
import std.string;
import std.conv;
import std.path;
import std.digest.md;
import std.base64;
const string MCU_ID = "atmega328p";
const ulong CPU_FREQ = 16_000_000;
const string BOOTLOADER = "arduino";
const ulong AVRDUDE_BAUDRATE = 57_600;
const string SOURCE_DIR = "src";
const string BUILD_DIR = "bin";
const string[] COMPILER_FLAGS = [
"-Wall",
"-Os",
"-g",
"-DF_CPU=16000000UL",
"-mmcu=atmega328p"
];
alias BuildCommand = int function(string[]);
int main(string[] args) {
string command;
if (args.length < 2) {
command = "help";
} else {
command = args[1].strip.toLower;
}
BuildCommand[string] commandMap = [
"build": &buildCommand,
"flash": &flashCommand,
"clean": &clean,
"help": &helpCommand
];
if (command !in commandMap) {
command = "help";
}
BuildCommand func = commandMap[command];
string[] commandArgs = [];
if (args.length > 2) commandArgs = args[2 .. $];
return func(commandArgs);
}
int helpCommand(string[] args) {
writeln("build.d - A simple build script for C files.");
writeln("The following commands are available:");
writeln("build [-f] - Compiles source code. Use -f to force rebuild.");
writeln(" By default, sources are hashed, and only built if changes are detected.");
writeln("flash [buildArgs] - Flashes code onto a connected AVR device via AVRDude.");
writeln("clean - Removes all build files.");
writeln("help - Shows this help information.");
return 0;
}
int clean(string[] args) {
rmdirRecurse(BUILD_DIR);
return 0;
}
int buildCommand(string[] args) {
import std.algorithm : canFind;
return build(canFind(args, "-f"));
}
int flashCommand(string[] args) {
int result = buildCommand(args);
if (result != 0) return result;
return flashToMCU(buildPath("bin", "gympal.hex"));
}
int build(bool force = false) {
import std.datetime.stopwatch;
if (!exists(BUILD_DIR)) mkdir(BUILD_DIR);
string[] sources = findFiles(SOURCE_DIR, ".c");
string[] objects;
objects.reserve(sources.length);
StopWatch sw = StopWatch(AutoStart.yes);
foreach (source; sources) {
objects ~= compileSourceToObject(source, force);
}
string elfFile = linkObjects(objects);
string hexFile = copyToHex(elfFile);
sw.stop();
ulong durationMillis = sw.peek().total!"msecs";
writefln!"Built %s in %d ms."(hexFile, durationMillis);
runOrQuit("avr-size " ~ hexFile);
return 0;
}
//-------- Utility functions below here -------
int run(string shellCommand) {
import std.process : Pid, spawnShell, wait;
Pid pid = spawnShell(shellCommand);
return wait(pid);
}
void runOrQuit(string shellCommand, int[] successExitCodes = [0]) {
import core.stdc.stdlib : exit;
import std.algorithm : canFind;
int result = run(shellCommand);
if (!canFind(successExitCodes, result)) exit(result);
}
void quitIfNonZero(int n) {
import core.stdc.stdlib : exit;
if (n != 0) exit(n);
}
bool shouldCompileSource(string sourcePath) {
string name = baseName(sourcePath);
name = name[0 .. name.lastIndexOf('.')];
string hashPath = buildPath("bin", "hash", name ~ ".md5");
string objectPath = buildPath("bin", name ~ ".o");
if (!exists(hashPath) || !exists(objectPath)) return true;
ubyte[] storedHash = Base64.decode(readText(hashPath).strip());
ubyte[16] currentHash = md5Of(readText(sourcePath));
return storedHash != currentHash;
}
string compileSourceToObject(string sourcePath, bool force = false) {
string flags = join(COMPILER_FLAGS, " ");
string name = baseName(sourcePath);
name = name[0 .. name.lastIndexOf('.')];
string objectPath = buildPath("bin", name ~ ".o");
string hashPath = buildPath("bin", "hash", name ~ ".md5");
if (!force && !shouldCompileSource(sourcePath)) {
writefln!"Not compiling %s because no changes detected."(sourcePath);
return objectPath;
}
string cmd = format!"avr-gcc %s -c -o %s %s"(
flags,
objectPath,
sourcePath
);
writeln(cmd);
runOrQuit(cmd);
ubyte[16] hash = md5Of(readText(sourcePath));
string hashDir = buildPath("bin", "hash");
if (!exists(hashDir)) mkdir(hashDir);
std.file.write(hashPath, Base64.encode(hash));
return objectPath;
}
string linkObjects(string[] objectPaths) {
string objectsArg = join(objectPaths, " ");
string flags = join(COMPILER_FLAGS, " ");
string elfFile = buildPath(BUILD_DIR, "gympal.elf");
string cmd = format!"avr-gcc %s -o %s %s"(
flags,
elfFile,
objectsArg
);
writeln(cmd);
runOrQuit(cmd);
return elfFile;
}
string copyToHex(string elfFile) {
string hexFile = buildPath(BUILD_DIR, "gympal.hex");
string cmd = format!"avr-objcopy -j .data -j .text -O ihex %s %s"(
elfFile,
hexFile
);
writeln(cmd);
runOrQuit(cmd);
return hexFile;
}
int flashToMCU(string hexFile) {
string cmd = format!"avrdude -c %s -p m328p -P /dev/ttyUSB0 -b %d -u -U flash:w:%s:i"(
BOOTLOADER,
AVRDUDE_BAUDRATE,
hexFile
);
writeln(cmd);
return run(cmd);
}
string[] findFiles(string path, string suffix = null) {
import std.array;
import std.algorithm;
auto app = appender!(string[]);
foreach (DirEntry entry; dirEntries(path, SpanMode.breadth, false)) {
if (entry.isFile && (suffix is null || entry.name.endsWith(suffix))) {
app ~= entry.name;
}
}
return app[];
}