2022-12-09 10:41:13 +00:00
|
|
|
#!/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;
|
2022-12-12 12:31:21 +00:00
|
|
|
import std.digest.md;
|
|
|
|
import std.base64;
|
2022-12-09 10:41:13 +00:00
|
|
|
|
|
|
|
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",
|
2023-01-03 18:40:20 +00:00
|
|
|
"-Os",
|
|
|
|
"-g",
|
|
|
|
"-DF_CPU=16000000UL",
|
|
|
|
"-mmcu=atmega328p"
|
2022-12-09 10:41:13 +00:00
|
|
|
];
|
|
|
|
|
2023-01-03 18:40:20 +00:00
|
|
|
alias BuildCommand = int function(string[]);
|
|
|
|
|
2022-12-09 10:41:13 +00:00
|
|
|
int main(string[] args) {
|
2023-01-03 18:40:20 +00:00
|
|
|
string command;
|
2022-12-09 10:44:27 +00:00
|
|
|
if (args.length < 2) {
|
2023-01-03 18:40:20 +00:00
|
|
|
command = "help";
|
|
|
|
} else {
|
|
|
|
command = args[1].strip.toLower;
|
2022-12-09 10:44:27 +00:00
|
|
|
}
|
2023-01-03 18:40:20 +00:00
|
|
|
|
|
|
|
BuildCommand[string] commandMap = [
|
|
|
|
"build": &buildCommand,
|
|
|
|
"flash": &flashCommand,
|
|
|
|
"clean": &clean,
|
|
|
|
"help": &helpCommand
|
|
|
|
];
|
|
|
|
|
|
|
|
if (command !in commandMap) {
|
|
|
|
command = "help";
|
2022-12-12 12:31:21 +00:00
|
|
|
}
|
2022-12-09 10:44:27 +00:00
|
|
|
|
2023-01-03 18:40:20 +00:00
|
|
|
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.");
|
2022-12-09 10:44:27 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2023-01-03 18:40:20 +00:00
|
|
|
int clean(string[] args) {
|
2022-12-09 10:44:27 +00:00
|
|
|
rmdirRecurse(BUILD_DIR);
|
2022-12-09 10:41:13 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2023-01-03 18:40:20 +00:00
|
|
|
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"));
|
|
|
|
}
|
|
|
|
|
2022-12-12 12:31:21 +00:00
|
|
|
int build(bool force = false) {
|
2023-01-04 07:48:45 +00:00
|
|
|
import std.datetime.stopwatch;
|
2022-12-09 10:41:13 +00:00
|
|
|
if (!exists(BUILD_DIR)) mkdir(BUILD_DIR);
|
|
|
|
string[] sources = findFiles(SOURCE_DIR, ".c");
|
|
|
|
string[] objects;
|
|
|
|
objects.reserve(sources.length);
|
2023-01-04 07:48:45 +00:00
|
|
|
StopWatch sw = StopWatch(AutoStart.yes);
|
2022-12-09 10:41:13 +00:00
|
|
|
foreach (source; sources) {
|
2022-12-12 12:31:21 +00:00
|
|
|
objects ~= compileSourceToObject(source, force);
|
2022-12-09 10:41:13 +00:00
|
|
|
}
|
|
|
|
string elfFile = linkObjects(objects);
|
|
|
|
string hexFile = copyToHex(elfFile);
|
2023-01-04 07:48:45 +00:00
|
|
|
sw.stop();
|
|
|
|
ulong durationMillis = sw.peek().total!"msecs";
|
|
|
|
writefln!"Built %s in %d ms."(hexFile, durationMillis);
|
2022-12-12 12:31:21 +00:00
|
|
|
runOrQuit("avr-size " ~ hexFile);
|
2022-12-09 10:41:13 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-12-12 12:31:21 +00:00
|
|
|
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) {
|
2022-12-09 10:41:13 +00:00
|
|
|
string flags = join(COMPILER_FLAGS, " ");
|
|
|
|
string name = baseName(sourcePath);
|
|
|
|
name = name[0 .. name.lastIndexOf('.')];
|
|
|
|
string objectPath = buildPath("bin", name ~ ".o");
|
2022-12-12 12:31:21 +00:00
|
|
|
string hashPath = buildPath("bin", "hash", name ~ ".md5");
|
|
|
|
|
|
|
|
if (!force && !shouldCompileSource(sourcePath)) {
|
|
|
|
writefln!"Not compiling %s because no changes detected."(sourcePath);
|
|
|
|
return objectPath;
|
|
|
|
}
|
|
|
|
|
2023-01-03 18:40:20 +00:00
|
|
|
string cmd = format!"avr-gcc %s -c -o %s %s"(
|
2022-12-09 10:41:13 +00:00
|
|
|
flags,
|
|
|
|
objectPath,
|
|
|
|
sourcePath
|
|
|
|
);
|
|
|
|
writeln(cmd);
|
|
|
|
runOrQuit(cmd);
|
2022-12-12 12:31:21 +00:00
|
|
|
|
|
|
|
ubyte[16] hash = md5Of(readText(sourcePath));
|
|
|
|
string hashDir = buildPath("bin", "hash");
|
|
|
|
if (!exists(hashDir)) mkdir(hashDir);
|
|
|
|
std.file.write(hashPath, Base64.encode(hash));
|
|
|
|
|
2022-12-09 10:41:13 +00:00
|
|
|
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");
|
2023-01-03 18:40:20 +00:00
|
|
|
string cmd = format!"avr-objcopy -j .data -j .text -O ihex %s %s"(
|
2022-12-09 10:41:13 +00:00
|
|
|
elfFile,
|
|
|
|
hexFile
|
|
|
|
);
|
|
|
|
writeln(cmd);
|
|
|
|
runOrQuit(cmd);
|
|
|
|
return hexFile;
|
|
|
|
}
|
|
|
|
|
|
|
|
int flashToMCU(string hexFile) {
|
2023-01-03 18:40:20 +00:00
|
|
|
string cmd = format!"avrdude -c %s -p m328p -P /dev/ttyUSB0 -b %d -u -U flash:w:%s:i"(
|
2022-12-09 10:41:13 +00:00
|
|
|
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[];
|
|
|
|
}
|