diff --git a/.gitignore b/.gitignore index e20dd3e..bb3dbb1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .sconsign.dblite *.elf *.hex +*.pyc build/ diff --git a/README.md b/README.md index fe208ec..1c7ec1a 100644 --- a/README.md +++ b/README.md @@ -9,22 +9,38 @@ build & upload Arduino sketch on the command line with scons! ## Basic Usage: -- make a folder which have same name of the sketch (ex. Blink/ for Blink.pde) -- put the sketch and the SConstruct under the folder. -- to make the HEX do following in the folder: + 1. Make a directory which have same name of the sketch (ex. Blink/ for Blink.pde) + * Note that, as a convenience, if the sketch does _not_ have the same name + as the parent directory, compilation will still be performed if it is the + only file in the directory with the extension `.pde` or `.ino` + 2. Put the sketch, `SConstruct`, and the directory `site_scons` under the + sketch directory. + 3. To compile the `.hex`-file, do following in the directory: - $ scons + $ scons -- to upload the binary, do following in the folder: + 4. To upload the binary, do following in the directory: - $ scons upload + $ scons upload -- refer [Expert Usage](https://github.com/suapapa/arscons/wiki/Expert-Usage) for change the confs. -- refer [Arscons Users](https://github.com/suapapa/arscons/wiki/Arscons-Users) for arscons in practice (and hacks!) + - Refer [Expert Usage](https://github.com/suapapa/arscons/wiki/Expert-Usage) + for change the confs. + - Refer [Arscons Users](https://github.com/suapapa/arscons/wiki/Arscons-Users) + for arscons in practice (and hacks!). +## `site_scons.arduino_build` module: + +In addition to the basic usage described above, the `site_scons.arduino_build` +module may be included in other projects using SCons to build any Arduino +components. The `site_scons` directory provides a way to integrate custom +builders and tools for SCons. See [here][1] for more information about +`site_scons` support in SCons. ## Thanks to: - Ovidiu Predescu and Lee Pike for Mac port and bugfix. - Steven Ashley for Windows port. - Kyle Gordon for many patches which including Arduino-1 support + + +[1]: http://www.scons.org/doc/1.0.1/HTML/scons-user/x3627.html diff --git a/SConstruct b/SConstruct index b671dab..1af25a1 100644 --- a/SConstruct +++ b/SConstruct @@ -1,28 +1,32 @@ -#!/usr/bin/python - # arscons: scons script for the Arduino sketch # http://github.com/suapapa/arscons # # Copyright (C) 2010-2013 by Homin Lee +# Copyright (C) 2013 by Christian Fobel # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. - # You'll need the serial module: http://pypi.python.org/pypi/pyserial # Basic Usage: -# 1. make a folder which have same name of the sketch (ex. Blink/ for Blink.pde) -# 2. put the sketch and SConstruct(this file) under the folder. -# 3. to make the HEX. do following in the folder. +# 1. Make a directory which have same name of the sketch (ex. Blink/ for Blink.pde) +# * Note that, as a convenience, if the sketch does _not_ have the same name +# as the parent directory, compilation will still be performed if it is the +# only file in the directory with the extension `.pde` or `.ino` +# 2. Put the sketch, `SConstruct` _(this file)_, and the directory `site_scons` +# under the sketch directory. +# 3. To make the HEX. do following in the directory. # $ scons -# 4. to upload the binary, do following in the folder. +# 4. To upload the binary, do following in the directory. # $ scons upload # Thanks to: # * Ovidiu Predescu and Lee Pike # for Mac port and bugfix. +# * Steven Ashley for Windows port. +# * Kyle Gordon for many patches which including Arduino-1 support # # This script tries to determine the port to which you have an Arduino # attached. If multiple USB serial devices are attached to your @@ -35,415 +39,8 @@ # to scons, like this: # # $ scons EXTRA_LIB= -# - -from glob import glob -from itertools import ifilter, imap -from os import path -from subprocess import check_call, CalledProcessError -import json -import os -import re -import sys - -# arscons version -__version__ = "1.0.0" - -env = Environment() -platform = env['PLATFORM'] - -VARTAB = {} - -try: - config = json.load(open('arscons.json')) -except IOError: - config = None - -def config_get(varname, returns): - if config: - result = config.get(varname, returns) - else: - result = returns - - return result - -def resolve_var(varname, default_value): - global VARTAB - # precedence: scons argument -> env. variable -> json config -> default value - ret = ARGUMENTS.get(varname, None) - VARTAB[varname] = ('arg', ret) - if ret == None: - ret = os.environ.get(varname, None) - VARTAB[varname] = ('env', ret) - if ret == None: - ret = config_get(varname, None) - VARTAB[varname] = ('cnf', ret) - if ret == None: - ret = default_value - VARTAB[varname] = ('dfl', ret) - return ret - -def getUsbTty(rx): - usb_ttys = glob(rx) - return usb_ttys[0] if len(usb_ttys) == 1 else None - -AVR_BIN_PREFIX = None -AVRDUDE_CONF = None -AVR_HOME_DUDE = None - -if platform == 'darwin': - # For MacOS X, pick up the AVR tools from within Arduino.app - ARDUINO_HOME = resolve_var('ARDUINO_HOME', - '/Applications/Arduino.app/Contents/Resources/Java') - ARDUINO_PORT = resolve_var('ARDUINO_PORT', getUsbTty('/dev/tty.usbserial*')) - SKETCHBOOK_HOME = resolve_var('SKETCHBOOK_HOME', '') - AVR_HOME = resolve_var('AVR_HOME', - path.join(ARDUINO_HOME, 'hardware/tools/avr/bin')) -elif platform == 'win32': - # For Windows, use environment variables. - ARDUINO_HOME = resolve_var('ARDUINO_HOME', None) - ARDUINO_PORT = resolve_var('ARDUINO_PORT', '') - SKETCHBOOK_HOME = resolve_var('SKETCHBOOK_HOME', '') - if ARDUINO_HOME: - AVR_HOME = resolve_var('AVR_HOME', - path.join(ARDUINO_HOME, 'hardware/tools/avr/bin')) -else: - # For Ubuntu Linux (9.10 or higher) - ARDUINO_HOME = resolve_var('ARDUINO_HOME', '/usr/share/arduino/') - ARDUINO_PORT = resolve_var('ARDUINO_PORT', getUsbTty('/dev/ttyUSB*')) - SKETCHBOOK_HOME = resolve_var('SKETCHBOOK_HOME', - path.expanduser('~/share/arduino/sketchbook/')) - AVR_HOME = resolve_var('AVR_HOME', - path.join(ARDUINO_HOME, 'hardware/tools/avr/bin')) - AVR_HOME_DUDE = resolve_var('AVR_HOME', - path.join(ARDUINO_HOME, 'hardware/tools/')) - -ARDUINO_BOARD = resolve_var('ARDUINO_BOARD', 'atmega328') -ARDUINO_VER = resolve_var('ARDUINO_VER', 0) # Default to 0 if nothing is specified -RST_TRIGGER = resolve_var('RST_TRIGGER', None) # use built-in pulseDTR() by default -EXTRA_LIB = resolve_var('EXTRA_LIB', None) # handy for adding another arduino-lib dir - -if not ARDUINO_HOME: - print 'ARDUINO_HOME must be defined.' - raise KeyError('ARDUINO_HOME') - -ARDUINO_CONF = path.join(ARDUINO_HOME, 'hardware/arduino/boards.txt') -# check given board name, ARDUINO_BOARD is valid one -arduino_boards = path.join(ARDUINO_HOME,'hardware/*/boards.txt') -custom_boards = path.join(SKETCHBOOK_HOME,'hardware/*/boards.txt') -board_files = glob(arduino_boards) + glob(custom_boards) -ptnBoard = re.compile(r'^([^#]*)\.name=(.*)') -boards = {} -for bf in board_files: - for line in open(bf): - result = ptnBoard.match(line) - if result: - boards[result.group(1)] = (result.group(2), bf) - -if ARDUINO_BOARD not in boards: - print "ERROR! the given board name, %s is not in the supported board list:" % ARDUINO_BOARD - print "all available board names are:" - for name, description in boards.iteritems(): - print "\t%s for %s" % (name.ljust(14), description[0]) - #print "however, you may edit %s to add a new board." % ARDUINO_CONF - sys.exit(-1) - -ARDUINO_CONF = boards[ARDUINO_BOARD][1] - -def getBoardConf(conf, default = None): - for line in open(ARDUINO_CONF): - line = line.strip() - if '=' in line: - key, value = line.split('=') - if key == '.'.join([ARDUINO_BOARD, conf]): - return value - ret = default - if ret == None: - print "ERROR! can't find %s in %s" % (conf, ARDUINO_CONF) - assert(False) - return ret - -ARDUINO_CORE = path.join(ARDUINO_HOME, path.dirname(ARDUINO_CONF), - 'cores/', getBoardConf('build.core', 'arduino')) -ARDUINO_SKEL = path.join(ARDUINO_CORE, 'main.cpp') - -if ARDUINO_VER == 0: - arduinoHeader = path.join(ARDUINO_CORE, 'Arduino.h') - #print "No Arduino version specified. Discovered version", - if path.exists(arduinoHeader): - #print "100 or above" - ARDUINO_VER = 100 - else: - #print "0023 or below" - ARDUINO_VER = 23 -else: - print "Arduino version " + ARDUINO_VER + " specified" - -# Some OSs need bundle with IDE tool-chain -if platform == 'darwin' or platform == 'win32': - AVRDUDE_CONF = path.join(ARDUINO_HOME, 'hardware/tools/avr/etc/avrdude.conf') - -AVR_BIN_PREFIX = path.join(AVR_HOME, 'avr-') - -ARDUINO_LIBS = [path.join(ARDUINO_HOME, 'libraries')] -if EXTRA_LIB: - ARDUINO_LIBS.append(EXTRA_LIB) -if SKETCHBOOK_HOME: - ARDUINO_LIBS.append(path.join(SKETCHBOOK_HOME, 'libraries')) - - -# Override MCU and F_CPU -MCU = ARGUMENTS.get('MCU', getBoardConf('build.mcu')) -F_CPU = ARGUMENTS.get('F_CPU', getBoardConf('build.f_cpu')) - -# There should be a file with the same name as the folder and -# with the extension .pde or .ino -# Or, one can specify it via the ARSCONS_TARGET environment -# variable.. - -TARGET = resolve_var('ARSCONS_TARGET', None) -if TARGET is None: - TARGET = path.basename(path.realpath(os.curdir)) - -assert(path.exists(TARGET + '.ino') or path.exists(TARGET + '.pde')) -sketchExt = '.ino' if path.exists(TARGET + '.ino') else '.pde' - -cFlags = ['-ffunction-sections', '-fdata-sections', '-fno-exceptions', - '-funsigned-char', '-funsigned-bitfields', '-fpack-struct', - '-fshort-enums', '-Os', '-Wall', '-mmcu=%s' % MCU] - -# Add some missing paths to CFLAGS -# Workaround for /usr/libexec/gcc/avr/ld: cannot open linker script file ldscripts/avr5.x: No such file or directory -# Workaround for /usr/libexec/gcc/avr/ld: crtm168.o: No such file: No such file or directory -extra_cflags = [ - '-L/usr/x86_64-pc-linux-gnu/avr/lib/', - '-B/usr/avr/lib/avr5/', - ] -cFlags += extra_cflags - -if ARDUINO_BOARD == "leonardo": - cFlags += ["-DUSB_VID="+getBoardConf('build.vid')] - cFlags += ["-DUSB_PID="+getBoardConf('build.pid')] - -envArduino = Environment(CC = AVR_BIN_PREFIX + 'gcc', - CXX = AVR_BIN_PREFIX + 'g++', - AS = AVR_BIN_PREFIX + 'gcc', - CPPPATH = ['build/core'], - CPPDEFINES = {'F_CPU': F_CPU, 'ARDUINO': ARDUINO_VER}, - CFLAGS = cFlags + ['-std=gnu99'], - CCFLAGS = cFlags, - ASFLAGS = ['-assembler-with-cpp','-mmcu=%s' % MCU], - TOOLS = ['gcc','g++', 'as']) - -hwVariant = path.join(ARDUINO_HOME, 'hardware/arduino/variants', - getBoardConf("build.variant", "")) -if hwVariant: - envArduino.Append(CPPPATH = hwVariant) - -# Show version -def printVersion(target, source, env): - print "arscons v%s"%__version__ - -version = envArduino.Alias('version', None, [printVersion]) -AlwaysBuild(version) - -def run(cmd): - """Run a command and decipher the return code. Exit by default.""" - # print ' '.join(cmd) - try: - check_call(cmd) - except CalledProcessError as cpe: - print "Error: return code: " + str(cpe.returncode) - sys.exit(cpe.returncode) - -# WindowXP not supported path.samefile -def sameFile(p1, p2): - if platform == 'win32': - ap1 = path.abspath(p1) - ap2 = path.abspath(p2) - return ap1 == ap2 - return path.samefile(p1, p2) - -def fnProcessing(target, source, env): - wp = open(str(target[0]), 'wb') - wp.write(open(ARDUINO_SKEL).read()) - - types='''void - int char word long - float double byte long - boolean - uint8_t uint16_t uint32_t - int8_t int16_t int32_t''' - types=' | '.join(types.split()) - re_signature = re.compile(r"""^\s* ( - (?: (%s) \s+ )? - \w+ \s* - \( \s* ((%s) \s+ \*? \w+ (?:\s*,\s*)? )* \) - ) \s* {? \s* $""" % (types, types), re.MULTILINE | re.VERBOSE) - - prototypes = {} - - for file in glob(path.realpath(os.curdir) + "/*" + sketchExt): - for line in open(file): - result = re_signature.search(line) - if result: - prototypes[result.group(1)] = result.group(2) - - for name in prototypes.iterkeys(): - print "%s;" % name - wp.write("%s;\n" % name) - - for file in glob(path.realpath(os.curdir) + "/*" + sketchExt): - print file, TARGET - if not sameFile(file, TARGET + sketchExt): - wp.write('#line 1 "%s"\r\n' % file) - wp.write(open(file).read()) - - # Add this preprocessor directive to localize the errors. - sourcePath = str(source[0]).replace('\\', '\\\\') - wp.write('#line 1 "%s"\r\n' % sourcePath) - wp.write(open(str(source[0])).read()) - -def fnCompressCore(target, source, env): - core_prefix = 'build/core/'.replace('/', os.path.sep) - core_files = (x for x in imap(str, source) - if x.startswith(core_prefix)) - for file in core_files: - run([AVR_BIN_PREFIX + 'ar', 'rcs', str(target[0]), file]) - -def fnPrintInfo(target, source, env): - for k in VARTAB: - cameFrom, value = VARTAB[k] - print "* %s: %s (%s)"%(k, value, cameFrom) - print "* avr-size:" - run([AVR_BIN_PREFIX + 'size', '--target=ihex', str(source[0])]) - # TODO: check binary size - print "* maximum size for hex file: %s bytes" % getBoardConf('upload.maximum_size') - - -bldProcessing = Builder(action = fnProcessing) #, suffix = '.cpp', src_suffix = sketchExt) -bldCompressCore = Builder(action = fnCompressCore) -bldELF = Builder(action = AVR_BIN_PREFIX + 'gcc -mmcu=%s ' % MCU + - '-Os -Wl,--gc-sections -lm %s -o $TARGET $SOURCES -lc' % ' '.join(extra_cflags)) -bldHEX = Builder(action = AVR_BIN_PREFIX + 'objcopy -O ihex -R .eeprom $SOURCES $TARGET') -bldInfo = Builder(action = fnPrintInfo) - -envArduino.Append(BUILDERS = {'Processing' : bldProcessing}) -envArduino.Append(BUILDERS = {'CompressCore': bldCompressCore}) -envArduino.Append(BUILDERS = {'Elf' : bldELF}) -envArduino.Append(BUILDERS = {'Hex' : bldHEX}) -envArduino.Append(BUILDERS = {'BuildInfo' : bldInfo}) - -ptnSource = re.compile(r'\.(?:c(?:pp)?|S)$') -def gatherSources(srcpath): - return [path.join(srcpath, f) for f - in os.listdir(srcpath) if ptnSource.search(f)] - -# add arduino core sources -VariantDir('build/core', ARDUINO_CORE) -core_sources = gatherSources(ARDUINO_CORE) -core_sources = [x.replace(ARDUINO_CORE, 'build/core/') for x - in core_sources if path.basename(x) != 'main.cpp'] - -# add libraries -libCandidates = [] -ptnLib = re.compile(r'^[ ]*#[ ]*include [<"](.*)\.h[>"]') -for line in open(TARGET + sketchExt): - result = ptnLib.search(line) - if not result: - continue - # Look for the library directory that contains the header. - filename = result.group(1) + '.h' - for libdir in ARDUINO_LIBS: - for root, dirs, files in os.walk(libdir, followlinks=True): - if filename in files: - libCandidates.append(path.basename(root)) - -# Hack. In version 20 of the Arduino IDE, the Ethernet library depends -# implicitly on the SPI library. -if ARDUINO_VER >= 20 and 'Ethernet' in libCandidates: - libCandidates.append('SPI') - -all_libs_sources = [] -for index, orig_lib_dir in enumerate(ARDUINO_LIBS): - lib_dir = 'build/lib_%02d' % index - VariantDir(lib_dir, orig_lib_dir) - for libPath in ifilter(path.isdir, glob(path.join(orig_lib_dir, '*'))): - libName = path.basename(libPath) - if not libName in libCandidates: - continue - envArduino.Append(CPPPATH = libPath.replace(orig_lib_dir, lib_dir)) - lib_sources = gatherSources(libPath) - utilDir = path.join(libPath, 'utility') - if path.exists(utilDir) and path.isdir(utilDir): - lib_sources += gatherSources(utilDir) - envArduino.Append(CPPPATH = utilDir.replace(orig_lib_dir, lib_dir)) - lib_sources = (x.replace(orig_lib_dir, lib_dir) for x in lib_sources) - all_libs_sources.extend(lib_sources) - -# Add raw sources which live in sketch dir. -build_top = path.realpath('.') -VariantDir('build/local/', build_top) -local_sources = gatherSources(build_top) -local_sources = [x.replace(build_top, 'build/local/') for x in local_sources] -if local_sources: - envArduino.Append(CPPPATH = 'build/local') - -# Convert sketch(.pde) to cpp -envArduino.Processing('build/' + TARGET + '.cpp', 'build/' + TARGET + sketchExt) -VariantDir('build', '.') - -sources = ['build/' + TARGET + '.cpp'] -#sources += core_sources -sources += local_sources -sources += all_libs_sources - -# Finally Build!! -core_objs = envArduino.Object(core_sources) -objs = envArduino.Object(sources) #, LIBS=libs, LIBPATH='.') -objs = objs + envArduino.CompressCore('build/core.a', core_objs) -envArduino.Elf(TARGET + '.elf', objs) -envArduino.Hex(TARGET + '.hex', TARGET + '.elf') -envArduino.BuildInfo(None, TARGET + '.hex') - -# Reset -def pulseDTR(target, source, env): - import serial - import time - ser = serial.Serial(ARDUINO_PORT) - ser.setDTR(1) - time.sleep(0.5) - ser.setDTR(0) - ser.close() - -if RST_TRIGGER: - reset_cmd = '%s %s' % (RST_TRIGGER, ARDUINO_PORT) -else: - reset_cmd = pulseDTR - -# Upload -UPLOAD_PROTOCOL = getBoardConf('upload.protocol') -UPLOAD_SPEED = getBoardConf('upload.speed') - -if UPLOAD_PROTOCOL == 'stk500': - UPLOAD_PROTOCOL = 'stk500v1' - - -avrdudeOpts = ['-V', '-F', '-c %s' % UPLOAD_PROTOCOL, '-b %s' % UPLOAD_SPEED, - '-p %s' % MCU, '-P %s' % ARDUINO_PORT, '-U flash:w:$SOURCES'] -if AVRDUDE_CONF: - avrdudeOpts.append('-C %s' % AVRDUDE_CONF) - -if AVR_HOME_DUDE: - AVR_BIN_PREFIX=AVR_HOME_DUDE - -fuse_cmd = '%s %s' % (path.join(path.dirname(AVR_BIN_PREFIX), 'avrdude'), - ' '.join(avrdudeOpts)) - -upload = envArduino.Alias('upload', TARGET + '.hex', [reset_cmd, fuse_cmd]) -AlwaysBuild(upload) +from arduino_build import ArduinoBuildContext -# Clean build directory -envArduino.Clean('all', 'build/') +context = ArduinoBuildContext(ARGUMENTS) -# vim: et sw=4 fenc=utf-8: +arduino_hex = context.build(register_upload=True) diff --git a/arscons.ino b/arscons.ino index 652b11d..e173592 100644 --- a/arscons.ino +++ b/arscons.ino @@ -1,4 +1,4 @@ -// This is just a mockup sketch for test scons works +// A mockup sketch to demonstrate building using arscons. #define PIN_LED 13 diff --git a/site_scons/__init__.py b/site_scons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/site_scons/arduino_build.py b/site_scons/arduino_build.py new file mode 100644 index 0000000..a93ab0f --- /dev/null +++ b/site_scons/arduino_build.py @@ -0,0 +1,605 @@ +# arscons: scons script for the Arduino sketch +# http://github.com/suapapa/arscons +# +# Copyright (C) 2010-2013 by Homin Lee +# Copyright (C) 2013 by Christian Fobel +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. + +# You'll need the serial module: http://pypi.python.org/pypi/pyserial + +# Basic Usage: +# 1. make a folder which have same name of the sketch (ex. Blink/ for Blik.pde) +# 2. put the sketch and SConstruct(this file) under the folder. +# 3. to make the HEX. do following in the folder. +# $ scons +# 4. to upload the binary, do following in the folder. +# $ scons upload + +# Thanks to: +# * Ovidiu Predescu and Lee Pike +# for Mac port and bugfix. +# +# This script tries to determine the port to which you have an Arduino +# attached. If multiple USB serial devices are attached to your +# computer, you'll need to explicitly specify the port to use, like +# this: +# +# $ scons ARDUINO_PORT=/dev/ttyUSB0 +# +# To add your own directory containing user libraries, pass EXTRA_LIB +# to scons, like this: +# +# $ scons EXTRA_LIB= +# +from glob import glob +import sys +import re +import os +import platform +from itertools import imap +from subprocess import check_call, CalledProcessError +import json + +from path import path +from SCons.Environment import Environment +from SCons.Builder import Builder + + +def run(cmd): + ''' + Run a command and decipher the return code. Exit by default. + ''' + print ' '.join(cmd) + try: + check_call(cmd) + except CalledProcessError as cpe: + print 'Error: return code: ' + str(cpe.returncode) + sys.exit(cpe.returncode) + + +# WindowXP not supported os.path.samefile +def same_file(p1, p2): + if platform.system() == 'Windows': + ap1 = os.path.abspath(p1) + ap2 = os.path.abspath(p2) + return ap1 == ap2 + return os.path.samefile(p1, p2) + + +def get_usb_tty(rx): + usb_ttys = glob(rx) + if len(usb_ttys) == 1: return usb_ttys[0] + else: return None + + +def gather_sources(source_root): + source_root = path(source_root) + return source_root.files('*.c') + source_root.files('*.cpp') +\ + source_root.files('*.S') + + +def get_lib_candidate_list(sketch_path, arduino_version): + ''' + Scan the .pde file to generate a list of included libraries. + ''' + # Generate list of library headers included in .pde file + lib_candidates = [] + ptn_lib = re.compile(r'^[ ]*#[ ]*include [<"](.*)\.h[>"]') + for line in open(sketch_path): + result = ptn_lib.findall(line) + if result: + lib_candidates += result + + # Hack. In version 20 of the Arduino IDE, the Ethernet library depends + # implicitly on the SPI library. + if arduino_version >= 20 and 'Ethernet' in lib_candidates: + lib_candidates += ['SPI'] + return lib_candidates + + +class ArduinoBuildContext(object): + ''' + Arduino SCons build-context class. + ================================== + + ## Usage ## + + 1. Create an instance of this class, passing in: + + * SCons arguments, i.e. `ARGUMENTS` + * A build directory path _(optional)_ + + 2. Call the `build` method, which returns a handle to the `.hex`-file + build rule. This handle can be used as a dependency in other SCons + build rules, etc. + ''' + def __init__(self, scons_arguments, build_root=None): + try: + self.config = json.load(open('arscons.json')) + except IOError: + self.config = None + + self.ARGUMENTS = scons_arguments + self.VARTAB = {} + + if build_root is None: + self.build_root = path('build') + else: + self.build_root = path(build_root) + self.build_root = self.build_root.abspath() + self.core_root = self.build_root.joinpath('core') + if not self.core_root.isdir(): + self.core_root.makedirs_p() + print '[build-core directory] %s (%s)' % (self.core_root, + self.core_root.isdir()) + self.resolve_config_vars() + + # Reset + def pulse_dtr(self, target, source, env): + import serial + import time + ser = serial.Serial(self.ARDUINO_PORT) + ser.setDTR(1) + time.sleep(0.5) + ser.setDTR(0) + ser.close() + + def resolve_config_vars(self): + self.AVR_BIN_PREFIX = None + self.AVRDUDE_CONF = None + self.AVR_HOME_DUDE = None + + if os.name == 'darwin': + # For MacOS X, pick up the AVR tools from within Arduino.app + self.ARDUINO_HOME = self.resolve_var('ARDUINO_HOME/Applications' + '/Arduino.app/Contents/' + 'Resources/Java') + self.ARDUINO_PORT = self.resolve_var('ARDUINO_PORT', + get_usb_tty('/dev/' + 'tty.usbserial*')) + self.SKETCHBOOK_HOME = self.resolve_var('SKETCHBOOK_HOME', '') + self.AVR_HOME = self.resolve_var('AVR_HOME', + os.path.join(self.ARDUINO_HOME, + 'hardware/tools/avr/' + 'bin')) + elif os.name == 'nt': + # For Windows, use environment variables. + self.ARDUINO_HOME = self.resolve_var('ARDUINO_HOME', None) + self.ARDUINO_PORT = self.resolve_var('ARDUINO_PORT', '') + self.SKETCHBOOK_HOME = self.resolve_var('SKETCHBOOK_HOME', '') + if self.ARDUINO_HOME: + self.AVR_HOME = self.resolve_var('AVR_HOME', + '%s' % os.path + .join(self.ARDUINO_HOME, + 'hardware', 'tools', + 'avr', 'bin')) + else: + # For Ubuntu Linux (12.04 or higher) + self.ARDUINO_HOME = self.resolve_var('ARDUINO_HOME', + '/usr/share/arduino/') + self.ARDUINO_PORT = self.resolve_var('ARDUINO_PORT', + get_usb_tty('/dev/ttyUSB*')) + default_sketchbook_home = os.path.expanduser('~/share/arduino/' + 'sketchbook/') + if not os.path.exists(default_sketchbook_home): + default_sketchbook_home = '' + self.SKETCHBOOK_HOME = self.resolve_var('SKETCHBOOK_HOME', + default_sketchbook_home) + self.AVR_HOME = self.resolve_var('AVR_HOME', '/usr/bin/') + self.AVR_HOME_DUDE = self.resolve_var('AVR_HOME', '/usr/bin/') + + self.ARDUINO_BOARD = self.resolve_var('ARDUINO_BOARD', 'uno') + # Default to 0 if nothing is specified + self.ARDUINO_VER = self.resolve_var('ARDUINO_VER', 0) + # use built-in pulse_dtr() by default + self.RST_TRIGGER = self.resolve_var('RST_TRIGGER', None) + # handy for adding another arduino-lib dir + self.EXTRA_LIB = self.resolve_var('EXTRA_LIB', None) + + if not self.ARDUINO_HOME: + print 'ARDUINO_HOME must be defined.' + raise KeyError('ARDUINO_HOME') + + self.ARDUINO_CONF = self.get_arduino_conf(self.ARDUINO_BOARD) + + self.ARDUINO_CORE = os.path.join(self.ARDUINO_HOME, + os.path.dirname(self.ARDUINO_CONF), + 'cores', + self.get_board_conf('build.core', + 'arduino')) + self.ARDUINO_SKEL = os.path.join(self.ARDUINO_CORE, 'main.cpp') + + if self.ARDUINO_VER == 0: + arduinoHeader = os.path.join(self.ARDUINO_CORE, 'Arduino.h') + #print "No Arduino version specified. Discovered version", + if os.path.exists(arduinoHeader): + #print "100 or above" + self.ARDUINO_VER = 100 + else: + #print "0023 or below" + self.ARDUINO_VER = 23 + else: + print "Arduino version " + self.ARDUINO_VER + " specified" + + # Some OSs need bundle with IDE tool-chain + if os.name == 'darwin' or os.name == 'nt': + self.AVRDUDE_CONF = os.path.join(self.ARDUINO_HOME, + 'hardware/tools/avr/etc/' + 'avrdude.conf') + + self.AVR_BIN_PREFIX = os.path.join(self.AVR_HOME, 'avr-') + + self.ARDUINO_LIBS = [os.path.join(self.ARDUINO_HOME, 'libraries')] + if self.EXTRA_LIB: + self.ARDUINO_LIBS.append(self.EXTRA_LIB) + if self.SKETCHBOOK_HOME: + self.ARDUINO_LIBS.append(os.path.join(self.SKETCHBOOK_HOME, 'libraries')) + + + # Override MCU and F_CPU + self.MCU = self.ARGUMENTS.get('MCU', self.get_board_conf('build.mcu')) + self.F_CPU = self.ARGUMENTS.get('F_CPU', self.get_board_conf('build.f_cpu')) + + + # Verify that there is a file with the same name as the folder and with + # the extension .pde + current_working_directory = path(os.getcwd()).name + + TARGET = None + + for possible_ext in ('.ino', '.pde'): + if os.path.exists(current_working_directory + possible_ext): + TARGET = current_working_directory + break + if TARGET is None: + for possible_ext in ('.ino', '.pde'): + possible_files = path('.').files('*' + possible_ext) + if len(possible_files) == 1: + # There is a single file that matches an Arduino file + # extension, so assume it is the main sketch file. + TARGET = possible_files[0].namebase + assert(TARGET is not None) + + print os.getcwd(), TARGET + + self.sketch_ext = '.ino' if os.path.exists(TARGET + '.ino') else '.pde' + self.TARGET = TARGET + self.sketch_path = path(TARGET + self.sketch_ext).abspath() + + def resolve_var(self, varname, default_value): + # precedence: scons argument -> env. variable -> json config -> default value + ret = self.ARGUMENTS.get(varname, None) + self.VARTAB[varname] = ('arg', ret) + if ret == None: + ret = os.environ.get(varname, None) + self.VARTAB[varname] = ('env', ret) + if ret == None: + ret = self.config_get(varname, None) + self.VARTAB[varname] = ('cnf', ret) + if ret == None: + ret = default_value + self.VARTAB[varname] = ('dfl', ret) + return ret + + def config_get(self, varname, returns): + if self.config: + result = self.config.get(varname, returns) + else: + result = returns + return result + + def get_arduino_conf(self, board_name): + # check given board name, ARDUINO_BOARD is valid one + arduino_boards = os.path.join(self.ARDUINO_HOME, 'hardware/*/boards.txt') + custom_boards = os.path.join(self.SKETCHBOOK_HOME, + 'hardware/*/boards.txt') + board_files = glob(arduino_boards) + glob(custom_boards) + board_cre = re.compile(r'^([^#]*)\.name=(.*)') + boards = {} + for bf in board_files: + for line in open(bf): + result = board_cre.match(line) + if result: + boards[result.group(1)] = (result.group(2), bf) + + if board_name not in boards: + print ('ERROR! the given board name, %s is not in the supported ' + 'board list:' % board_name) + print "all available board names are:" + for name, description in boards.iteritems(): + print "\t%s for %s" % (name.ljust(14), description[0]) + #print "however, you may edit %s to add a new board." % ARDUINO_CONF + sys.exit(-1) + return boards[board_name][1] + + def get_board_conf(self, conf, default=None): + for line in open(self.ARDUINO_CONF): + line = line.strip() + if '=' in line: + key, value = line.split('=') + if key == '.'.join([self.ARDUINO_BOARD, conf]): + return value + ret = default + if ret == None: + print "ERROR! can't find %s in %s" % (conf, self.ARDUINO_CONF) + assert(False) + return ret + + def get_env(self, **kwargs): + c_flags = ['-ffunction-sections', '-fdata-sections', '-fno-exceptions', + '-funsigned-char', '-funsigned-bitfields', '-fpack-struct', + '-fshort-enums', '-Os', '-Wall', '-mmcu=%s' % self.MCU] + + # Add some missing paths to CFLAGS + # Workaround for /usr/libexec/gcc/avr/ld: + # cannot open linker script file ldscripts/avr5.x: No such file or directory + # Workaround for /usr/libexec/gcc/avr/ld: + # crtm168.o: No such file: No such file or directory + extra_cflags = ['-L/usr/x86_64-pc-linux-gnu/avr/lib/', + '-B/usr/avr/lib/avr5/', ] + c_flags += extra_cflags + + if self.ARDUINO_BOARD == "leonardo": + c_flags += ["-DUSB_VID=" + self.get_board_conf('build.vid')] + c_flags += ["-DUSB_PID=" + self.get_board_conf('build.pid')] + + env_defaults = dict(CC=self.AVR_BIN_PREFIX + 'gcc', + CXX=self.AVR_BIN_PREFIX + 'g++', + AS=self.AVR_BIN_PREFIX + 'gcc', + CPPPATH=[self.core_root], + CPPDEFINES={'F_CPU': self.F_CPU, 'ARDUINO': + self.ARDUINO_VER}, + CFLAGS=c_flags + ['-std=gnu99'], CCFLAGS=c_flags, + ASFLAGS=['-assembler-with-cpp','-mmcu=%s' % + self.MCU], TOOLS=['gcc','g++', 'as']) + + hw_variant = os.path.join(self.ARDUINO_HOME, + 'hardware/arduino/variants', + self.get_board_conf('build.variant', '')) + if hw_variant: + env_defaults['CPPPATH'].append(hw_variant) + + for k, v in kwargs.iteritems(): + print 'processing kwarg: %s->%s' % (k, v) + if k in env_defaults and isinstance(env_defaults[k], dict)\ + and isinstance(v, dict): + env_defaults[k].update(v) + print ' update dict' + elif k in env_defaults and isinstance(env_defaults[k], list): + env_defaults[k].append(v) + print ' append to list' + else: + env_defaults[k] = v + print ' set value' + print 'kwargs:', kwargs + print 'env_defaults:', env_defaults + env_arduino = Environment(**env_defaults) + # Add Arduino Processing, Elf, and Hex builders to environment + for builder_name in ['Processing', 'CompressCore', 'Elf', 'Hex', + 'BuildInfo']: + env_arduino.Append(BUILDERS={builder_name: getattr(self, + 'get_%s_builder' + % builder_name + .lower())()}) + return env_arduino + + def get_avrdude_options(self): + upload_protocol = self.get_board_conf('upload.protocol') + upload_speed = self.get_board_conf('upload.speed') + + avrdude_opts = ['-V', '-F', '-c %s' % upload_protocol, + '-b %s' % upload_speed, '-p %s' % self.MCU, + '-P %s' % self.ARDUINO_PORT, + '-U flash:w:$SOURCES'] + + if self.AVRDUDE_CONF: + avrdude_opts += ['-C "%s"' % self.AVRDUDE_CONF] + return avrdude_opts + + def get_core_sources(self): + ''' + Generate list of Arduino core source files + ''' + core_sources = gather_sources(self.ARDUINO_CORE) + core_sources = [x.replace(self.ARDUINO_CORE, self.core_root) for x in + core_sources if os.path.basename(x) != 'main.cpp'] + return core_sources + + def get_lib_sources(self, env): + ''' + Add VariantDir references for all libraries in lib_candidates to the + corresponding paths in arduino_libs. + + Return the combined list of source files for all libraries, relative + to the respective VariantDir. + ''' + lib_candidates = get_lib_candidate_list(self.sketch_path, + self.ARDUINO_VER) + all_libs_sources = [] + all_lib_names = set() + index = 0 + for orig_lib_dir in self.ARDUINO_LIBS: + lib_sources = [] + lib_dir = self.build_root.joinpath('lib_%02d' % index) + print 'build_root: %s' % self.build_root + env.VariantDir(lib_dir, orig_lib_dir) + for lib_path in path(orig_lib_dir).dirs(): + lib_name = lib_path.name + if not lib_name in lib_candidates: + # This library is not included in the .pde file, so skip it + continue + elif lib_name in all_lib_names: + # This library has already been processed, so skip it + continue + all_lib_names.add(lib_name) + env.Append(CPPPATH=lib_path.replace(orig_lib_dir, lib_dir)) + lib_sources = gather_sources(lib_path) + util_dir = path(lib_path).joinpath('utility') + if os.path.exists(util_dir) and os.path.isdir(util_dir): + lib_sources += gather_sources(util_dir) + env.Append(CPPPATH=util_dir.replace(orig_lib_dir, lib_dir)) + lib_sources = [x.replace(orig_lib_dir, lib_dir) for x in lib_sources] + all_libs_sources += lib_sources + index += 1 + return all_libs_sources + + # ----------------------------------- + # Builders + # ======== + def get_processing_builder(self): + return Builder(action=self.processing_action) + + def get_compresscore_builder(self): + return Builder(action=self.compress_core_action) + + def get_elf_builder(self): + return Builder(action='"%s"' % (self.AVR_BIN_PREFIX + 'gcc') + + ' -mmcu=%s -Os -Wl,--gc-sections -o $TARGET $SOURCES ' + '-lm' % ( self.MCU)) + + def get_hex_builder(self): + return Builder(action='"%s"' % (self.AVR_BIN_PREFIX + 'objcopy') + + ' -O ihex -R .eeprom $SOURCES $TARGET') + + def get_buildinfo_builder(self): + return Builder(action=self.print_info_action) + + def compress_core_action(self, target, source, env): + import re + #core_pattern = re.compile(r'build.*/core/'.replace('/', os.path.sep)) + core_pattern = re.compile(r'build.*core') + core_files = (x for x in imap(str, source) if core_pattern.search(x)) + target_path = path(target[0]).abspath() + if not target_path.parent.isdir(): + target_path.parent.makedirs_p() + for core_file in core_files: + core_file_path = path(core_file).abspath() + print '[compress_core_action]', core_file_path, core_file_path.isfile() + command = [self.AVR_BIN_PREFIX + 'ar', 'rcs', target_path, + core_file_path] + run(command) + + def print_info_action(self, target, source, env): + for k in self.VARTAB: + came_from, value = self.VARTAB[k] + print '* %s: %s (%s)' % (k, value, came_from) + print '* avr-size:' + run([self.AVR_BIN_PREFIX + 'size', '--target=ihex', str(source[0])]) + # TODO: check binary size + print ('* maximum size for hex file: %s bytes' % + self.get_board_conf('upload.maximum_size')) + + def processing_action(self, target, source, env): + wp = open(str(target[0]), 'wb') + wp.write(open(self.ARDUINO_SKEL).read()) + + types='''void + int char word long + float double byte long + boolean + uint8_t uint16_t uint32_t + int8_t int16_t int32_t''' + types=' | '.join(types.split()) + re_signature = re.compile(r'''^\s* ( + (?: (%s) \s+ )? + \w+ \s* + \( \s* ((%s) \s+ \*? \w+ (?:\s*,\s*)? )* \) + ) \s* {? \s* $''' % (types, types), re.MULTILINE | re.VERBOSE) + + prototypes = {} + + for file in glob(os.path.realpath(os.curdir) + '/*' + self.sketch_ext): + for line in open(file): + result = re_signature.search(line) + if result: + prototypes[result.group(1)] = result.group(2) + + for name in prototypes.iterkeys(): + print '%s;' % name + wp.write('%s;\n' % name) + + for file in glob(os.path.realpath(os.curdir) + '/*' + self.sketch_ext): + print file, self.sketch_path + if not same_file(file, self.sketch_path): + wp.write('#line 1 "%s"\r\n' % file) + wp.write(open(file).read()) + + # Add this preprocessor directive to localize the errors. + source_path = str(source[0]).replace('\\', '\\\\') + wp.write('#line 1 "%s"\r\n' % source_path) + wp.write(open(str(source[0])).read()) + + # ----------------------------------- + def build(self, hex_root=None, env_dict=None, extra_sources=None, + register_upload=False): + ''' + Return handle to built `.hex`-file rule. + + The contents of `env_dict` _(if provided)_ are used to update the SCons + `Environment` used when compiling the Arduino project. + ''' + if hex_root is None: + hex_root = self.build_root.joinpath('hex') + else: + hex_root = path(hex_root) + if not hex_root.isabs(): + hex_root = self.build_root.joinpath(hex_root) + hex_path = hex_root.joinpath(self.TARGET + '.hex') + + if env_dict is None: + env_dict = {} + env = self.get_env(**env_dict) + print env['CPPDEFINES'] + + # Convert sketch(.pde) to cpp + env.Processing(hex_root.joinpath(self.TARGET + '.cpp'), + hex_root.joinpath(self.sketch_path)) + + sources = [hex_root.joinpath(self.TARGET+'.cpp')] + sources += self.get_lib_sources(env) + if extra_sources: + sources += [hex_root.joinpath(s) for s in extra_sources] + + # Finally Build!! + core_sources = self.get_core_sources() + + core_objs = env.Object(core_sources) + objs = env.Object(sources) + objs += env.CompressCore(self.core_root.joinpath('core.a').abspath(), + core_objs) + + elf_path = hex_root.joinpath(self.TARGET + '.elf') + env.Elf(elf_path, objs) + arduino_hex = env.Hex(hex_path, hex_root.joinpath(self.TARGET + '.elf')) + + # Print Size + # TODO: check binary size + MAX_SIZE = self.get_board_conf('upload.maximum_size') + print ("maximum size for hex file: %s bytes" % MAX_SIZE) + env.BuildInfo(None, hex_path) + + if register_upload: + fuse_cmd = '%s %s' % (os.path.join(os.path + .dirname(self.AVR_BIN_PREFIX), + 'avrdude'), + ' '.join(self.get_avrdude_options())) + print fuse_cmd + + if self.RST_TRIGGER: + reset_cmd = '%s %s' % (self.RST_TRIGGER, self.ARDUINO_PORT) + else: + reset_cmd = self.pulse_dtr + + upload = env.Alias('upload', hex_path, [reset_cmd, fuse_cmd]) + env.AlwaysBuild(upload) + + # Clean build directory + env.Clean('all', 'build/') + + env.VariantDir(self.core_root, self.ARDUINO_CORE) + env.VariantDir(hex_root, '.') + return arduino_hex