package pif.arduino;
import java.io.*;
import java.util.List;
import org.apache.log4j.Logger;
import org.apache.commons.cli.*;
import pif.arduino.tools.ArduinoConfig;
import pif.arduino.tools.LoadConfig;
import processing.app.BaseNoGui;
import processing.app.debug.TargetBoard;
import processing.app.debug.TargetPlatform;
import processing.app.helpers.PreferencesMap;
import processing.app.helpers.StringReplacer;
public class MakeMake {
private static Logger logger = Logger.getLogger(MakeMake.class);
static Options options;
static {
options = new Options();
options.addOption("h", "help", false, "usage");
options.addOption("I", "arduino-ide", true, "base installation directory of Arduino IDE");
options.addOption(LoadConfig.PREFERENCES_OPTION, false, "alternate Arduino IDE preferences file");
OptionGroup choice = new OptionGroup();
choice.addOption(new Option("b", "board", true, "target board name"));
choice.addOption(new Option("B", "boards", false, "list available boards"));
choice.setRequired(true);
options.addOptionGroup(choice);
options.addOption("o", "output", true, "output directory for generated files");
options.addOption("d", "debug", false, "set debug level");
}
public static void main(String[] args) {
// -- mandatory option(s)
CommandLine commandLine = null;
try {
commandLine = new BasicParser().parse(options, args);
} catch (ParseException e) {
logger.error(e);
usage(1);
}
if (commandLine.hasOption('h')) {
usage(0);
}
if (commandLine.hasOption('d')) {
Logger.getRootLogger().setLevel(org.apache.log4j.Level.DEBUG);
}
// for debug of jna init
logger.debug("temp path = " + System.getProperty("java.io.tmpdir"));
if (!LoadConfig.load(commandLine)) {
usage(2);
}
if (commandLine.hasOption('B')) {
ArduinoConfig.listBoards(System.out, false, false);
} else {
generateBoard(
commandLine.getOptionValue('b'), // board
commandLine.getOptionValue('o'));// output directory
}
}
public static String makefileName(String boardName) {
return "Makefile.target." + boardName;
}
protected static void generateBoard(String boardName, String outdir) {
TargetBoard board = ArduinoConfig.setBoard(boardName);
if (board == null) {
usage(3);
}
TargetPlatform pf = board.getContainerPlatform();
logger.info(String.format("found board %s:%s:%s", pf.getContainerPackage().getId(), pf.getId(), board.getId()));
File outFile = new File(outdir == null ? "." : outdir);
if (outFile.isDirectory()) {
outFile = new File(outFile, makefileName(boardName));
} else if (!outFile.getParentFile().isDirectory()) {
logger.fatal("outdir must be an existing directory or a file name in existing directory");
System.exit(4);
}
PrintWriter out = null;
try {
out = new PrintWriter(outFile);
} catch (FileNotFoundException e) {
logger.fatal("Can't open output file", e);
System.exit(4);
}
// SketchData sketch = new SketchData(new File("toto.cpp"));
PreferencesMap prefs;
try {
// arguments are unused, we must just instantiate Compiler class to access to preferences
processing.app.debug.Compiler compiler = new processing.app.debug.Compiler(null, "${TARGET_DIR}", "${OUT_NAME}");
prefs = compiler.getBuildPreferences();
} catch (Exception e) {
e.printStackTrace();
return;
}
prefs.put("ide_version", "" + BaseNoGui.REVISION);
ArduinoConfig.changePathSeparators(prefs);
logger.debug("prefs = " + prefs);
List<File> libPath = ArduinoConfig.getLibrariesPath();
StringBuffer libPathString = new StringBuffer();
// don't use Java 8 String.join method since Arduino IDE comes with Java 6
for (int i = 0; i < libPath.size(); i++) {
if (i != 0) {
libPathString.append(" ");
}
libPathString.append(libPath.get(i));
}
logger.debug("libPath = " + libPathString);
preferencesHelper helper = new preferencesHelper(prefs, out);
logger.debug("generating board file");
helper.format("## autogenerated makefile rules for package %s, platform %s, board %s\n",
pf.getContainerPackage().getId(), pf.getId(), board.getId());
// modify some pathes to make them relatives to other variables
String idePath = helper.get("runtime.ide.path");
helper.changeRoot("runtime.hardware.path", idePath, "${ARDUINO_IDE}");
helper.changeRoot("build.core.path", idePath, "${ARDUINO_IDE}");
helper.changeRoot("build.variant.path", idePath, "${ARDUINO_IDE}");
helper.pref2varAndSet("ARDUINO_IDE", "runtime.ide.path");
helper.pref2varAndSet("TOOLCHAIN_DIR", "compiler.path");
helper.println("\n## entry point for core compilation");
helper.raw2var("TARGET_PACKAGE", pf.getContainerPackage().getId());
helper.raw2var("TARGET_PLATFORM", pf.getId());
helper.raw2var("TARGET_MCU", "build.mcu");
helper.raw2var("TARGET_F_CPU", "build.f_cpu");
helper.raw2var("TARGET_BAUDRATE", "upload.speed");
helper.pref2varAndSet("HARDWARE_DIR", "runtime.hardware.path");
helper.pref2varAndSet("CORE_DIR", "build.core.path");
helper.pref2varAndSet("VARIANT_DIR", "build.variant.path");
helper.raw2var("LIBRARIES_DIRS", libPathString.toString().replace('\\', '/'));
helper.raw2var("INCLUDE_FLAGS", "-I${CORE_DIR} -I${VARIANT_DIR}");
out.println("\n## C/C++ compiler");
helper.recipe2var("CC", "{compiler.path}{compiler.c.cmd}");
helper.pref2varAndSet("CFLAGS", "compiler.c.flags");
helper.recipe2var("CXX", "{compiler.path}{compiler.cpp.cmd}");
helper.pref2varAndSet("CXXFLAGS", "compiler.cpp.flags");
helper.recipe2var("AS", "{compiler.path}{compiler.c.cmd}");
helper.pref2varAndSet("ASFLAGS", "compiler.S.flags");
// try to cut command line to get specific options
// as these options are only in global recipe, we try to extract them by faking some {} entries
// and use them as separator "around" other arguments.
out.println("\n## flags to use from macro/includes discovery");
String extra = " " + helper.recipe("build.extra_flags");
String targetFlags = helper.recipe("recipe.cpp.o.pattern",
"compiler.cpp.flags", "<<<BEGIN>>>",
"compiler.cpp.extra_flags", "<<<END>>>");
targetFlags = targetFlags.replaceFirst("^.*<<<BEGIN>>>(.*)<<<END>>>.*$", "$1");
helper.raw2var("DISCOVERY_FLAGS_GXX", targetFlags + extra);
targetFlags = helper.recipe("recipe.c.o.pattern",
"compiler.c.flags", "<<<BEGIN>>>",
"compiler.c.extra_flags", "<<<END>>>");
targetFlags = targetFlags.replaceFirst("^.*<<<BEGIN>>>(.*)<<<END>>>.*$", "$1");
helper.raw2var("DISCOVERY_FLAGS_GCC", targetFlags + extra);
// target specific flags are into recipe itself and not in cflags definition
// => have to generate a full rule
out.println("\n## generate code from c, cpp, ino or S files");
out.println("${TARGET_DIR}/%.o: %.c");
out.println("\t@${MKDIR} ${@D}");
out.println("\t${BIN_PREFIX}" + helper.recipe("recipe.c.o.pattern",
"includes", "${INCLUDE_FLAGS} ${INCLUDE_FLAGS_EXTRA}",
"+compiler.c.extra_flags", "${CFLAGS_EXTRA}",
"source_file", "$(call truepath,$(call truepath,$<))",
"object_file", "$@"));
out.println("${TARGET_DIR}/%.o: %.ino");
out.println("\t@${MKDIR} ${@D}");
out.println("\t${BIN_PREFIX}" + helper.recipe("recipe.cpp.o.pattern",
"includes", "${INCLUDE_FLAGS} ${INCLUDE_FLAGS_EXTRA}",
"+compiler.cpp.extra_flags", "${CXXFLAGS_EXTRA} -x c++",
"source_file", "$(call truepath,$<)",
"object_file", "$@"));
out.println("${TARGET_DIR}/%.o: %.cpp");
out.println("\t@${MKDIR} ${@D}");
out.println("\t${BIN_PREFIX}" + helper.recipe("recipe.cpp.o.pattern",
"includes", "${INCLUDE_FLAGS} ${INCLUDE_FLAGS_EXTRA}",
"+compiler.cpp.extra_flags", "${CXXFLAGS_EXTRA}",
"source_file", "$(call truepath,$<)",
"object_file", "$@"));
out.println("${TARGET_DIR}/%.o: %.S");
out.println("\t@${MKDIR} ${@D}");
String recipe = helper.recipe("recipe.S.o.pattern",
"includes", "${INCLUDE_FLAGS} ${INCLUDE_FLAGS_EXTRA}",
"+compiler.S.extra_flags", "${ASFLAGS_EXTRA}",
"source_file", "$(call truepath,$<)",
"object_file", "$@");
if (recipe == null) {
out.println("\t$(error No rule to compile this kind of file for this target platform)");
} else {
out.println("\t${BIN_PREFIX}" + recipe);
}
out.println("\n## generate library");
helper.recipe2var("AR", "{compiler.path}{compiler.ar.cmd}");
helper.pref2varAndSet("ARFLAGS", "compiler.ar.flags");
out.println("${TARGET_DIR}/lib%.a:");
out.println("\t${BIN_PREFIX}${AR} ${ARFLAGS} ${ARFLAGS_EXTRA} $@ ${OBJS}");
// command line is a bit crazy
// => have to generate a full rule
out.println("\n## generate binary from .o files");
helper.pref2varAndSet("LDFLAGS", "compiler.c.elf.flags");
out.println("${TARGET_DIR}/%.elf: objects");
// recipes contain a "{build.path}/{archive_file}" to include core lib, but it doesn't match our path constraints
// => fake it by putting last object in it, and other ones in {object_files}
// + have to remove target_dir from this last entry
// out.println("\t" + helper.recipe("recipe.c.combine.pattern",
// "+compiler.c.elf.extra_flags", "${ELFFLAGS}",
// "object_files", "$(filter-out $(lastword $^),$^)", // /!\ with 's'
// "archive_file", "$(subst ${TARGET_DIR}/,,$(lastword $^))") + " ${LDFLAGS}");
out.println("\t${BIN_PREFIX}" + helper.recipe("recipe.c.combine.pattern",
"+compiler.c.elf.extra_flags", "",
"object_files", "${OBJS}", // /!\ with 's'
"archive_file", "${CORE_LIB_NAME}") + " ${LDFLAGS_EXTRA}");
out.println("\n## convert elf file");
helper.recipe2var("EEP", "{compiler.path}{compiler.objcopy.cmd}");
helper.pref2varAndSet("EEPFLAGS", "compiler.objcopy.eep.flags");
out.println("%.eep:%.elf");
out.println("\t${BIN_PREFIX}${EEP} ${EEPFLAGS} ${EEPFLAGS_EXTRA} $(call truepath,$<) $@");
String hexRecipe = helper.get("recipe.objcopy.hex.pattern");
if (hexRecipe.contains(".hex")) {
helper.raw2var("UPLOAD_EXT", ".hex");
helper.recipe2var("HEX", "{compiler.path}{compiler.elf2hex.cmd}");
helper.pref2varAndSet("HEXFLAGS", "compiler.elf2hex.flags");
out.println("%.hex:%.elf %.size");
out.println("\t${BIN_PREFIX}${HEX} ${HEXFLAGS} ${HEXFLAGS_EXTRA} $(call truepath,$<) $@");
} else if (hexRecipe.contains(".bin")) {
helper.raw2var("UPLOAD_EXT", ".bin");
helper.recipe2var("BIN", "{compiler.path}{compiler.elf2hex.cmd}");
helper.pref2varAndSet("BINFLAGS", "compiler.elf2hex.flags");
out.println("%.bin:%.elf");
out.println("\t${BIN_PREFIX}${BIN} ${BINFLAGS} ${BINFLAGS_EXTRA} $(call truepath,$<) $@");
} else {
helper.raw2var("UPLOAD_EXT", "_UNKNOWN");
out.println("$(warning don't know how to generate file to upload)");
}
helper.recipe2var("SIZE", "{compiler.path}{compiler.size.cmd}");
// this command has no flags preference
helper.raw2var("SIZEFLAGS", "-A");
out.println("%.size:%.elf");
out.println("\t${BIN_PREFIX}${SIZE} ${SIZEFLAGS} ${SIZEFLAGS_EXTRA} $(call truepath,$<) > $@");
out.println("\tcat $@");
out.println("\n## end of file");
out.close();
}
/**
* helper class to generate rules from preferences map
*/
static class preferencesHelper {
protected PreferencesMap prefs;
protected PrintWriter out;
/**
* create a new processor for given PreferencesMap
* @param prefs map into which recipes and variables can be found
* @param out PrintWriter where outputs will be printed
*/
public preferencesHelper(PreferencesMap prefs, PrintWriter out) {
this.prefs = prefs;
this.out = out;
}
/**
* have to wrap source preferences get() method, since it may be modified by other methods
*/
public String get(String pref) {
return prefs.get(pref);
}
/**
* replace preferences entry
*/
public void set(String pref, String value) {
prefs.put(pref, value);
}
/**
* append text to preferences entry, or set it if it wasn't defined already
*/
public void append(String pref, String value) {
String old = prefs.get(pref);
if (old == null) {
set(pref, value);
} else {
set(pref, old + value);
}
}
/**
* recursively replace references into a string
* @param pref name of template to get from preferences
* @param arguments list of successive key / value pairs specifying specific entry to add in map
* if a key starts with character '+', value is appended to existing map entry if exists, else it replaces its value
* @return translated string
* @throws Exception
*/
public String interpret(String source, String... arguments) {
PreferencesMap dict = new PreferencesMap(prefs);
for(int i = 0; i < arguments.length; i += 2) {
String key = arguments[i];
String value = arguments[i + 1];
if (key.charAt(0) == '+') {
key = key.substring(1);
if (dict.containsKey(key)) {
value = dict.get(key) + " " + value;
}
}
dict.put(key, value);
}
String result;
int retries = 10;
result = StringReplacer.replaceFromMapping(source, dict);
while (retries != 0 && !source.equals(result)) {
source = result;
result = StringReplacer.replaceFromMapping(source, dict);
retries--;
}
if (retries == 0) {
logger.warn("Catched max retries : infinite loop ?");
}
return result;
}
/**
* dump a "var := value" rule into current Writer
* @param var out variable name
* @param value variable value
*/
public void raw2var(String var, String value) {
out.println(String.format("%s = %s", var, value));
}
/**
* dump a "var := value" rule into current Writer
* @param var out variable name
* @param pref preference name to get as value
*/
public void pref2var(String var, String pref, String... arguments) {
String recipe = prefs.get(pref);
if (recipe == null) {
logger.warn("preference '" + pref + "' not found");
return;
}
String value = interpret(recipe, arguments);
raw2var(var, value);
}
/**
* dump a "var := value" rule into current Writer THEN replace preference value by reference to this variable
* @param var out variable name
* @param pref preference name to get as value
*/
public void pref2varAndSet(String var, String pref, String... arguments) {
String recipe = prefs.get(pref);
if (recipe == null) {
logger.warn("preference '" + pref + "' not found");
return;
}
String value = interpret(recipe, arguments);
raw2var(var, value);
set(pref, String.format("${%s}", var));
}
/**
* dump a "var := value" rule into current Writer
* @param var out variable name
* @param recipe string to interpret as value
*/
public void recipe2var(String var, String recipe, String... arguments) {
String value = interpret(recipe, arguments);
raw2var(var, value);
}
/**
* change start of given path in another value (ie variable value)
* @param pref preference to modify
* @param oldPrefix prefix path to replace
* @param newPrefix new value for this prefix
*/
public void changeRoot(String pref, String oldPrefix, String newPrefix) {
String path = prefs.get(pref);
if (path == null) {
logger.warn("preference '" + pref + "' not found");
return;
}
prefs.put(pref, path.replace(oldPrefix, newPrefix));
}
/**
* translate template string according to preferences and optional arguments
* @param prefs preferences map containing strings templates to replace with
* @param recipeName name of template to get from preferences
* @param arguments list of successive key / value pairs specifying specific entry to add in map
* if a key starts with character '+', value is appended to existing map entry if exists, else it replaces its value
* @return translated string
* @throws Exception
*/
public String recipe(String recipeName, String... arguments) {
String recipe = prefs.get(recipeName);
if (recipe == null) {
return null;
}
return interpret(recipe, arguments);
}
public void println(String str) {
out.println(str);
}
public void format(String format, Object... args) {
out.println(String.format(format, args));
}
}
protected static void usage(int exitCode) {
HelpFormatter fmt = new HelpFormatter();
String footer =
"\narduino ide path is looked for respectivly in command line option, java properties (-DARDUINO_IDE=...)," +
" ARDUINO_IDE environment variable, location of BaseNoGui arduino class if already loaded" +
" (if classpath was set accordingly for example)";
fmt.printHelp(80, "java -jar ArduinoMakeMake.jar options ...", "options :", options, footer);
System.exit(exitCode);
}
}