#!/usr/bin/python # arscons: SCons script for Arduino # http://github.com/suapapa/arscons # # Copyright (C) 2010-2013 by Homin Lee # # 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 with the same name as the sketch (ex. Blink/ for Blink.ino) # 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 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 is None: ret = os.environ.get(varname, None) VARTAB[varname] = ('env', ret) if ret is None: ret = config_get(varname, None) VARTAB[varname] = ('cnf', ret) if ret is 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/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 if given board name, ARDUINO_BOARD is a 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 boards 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 is 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 = 105 else: #print "0023 or below" ARDUINO_VER = 23 else: print "Arduino version " + ARDUINO_VER + " specified" # On some OSs we need to reuse parts of the original IDE tool-chain if platform == 'darwin' or platform == 'win32': AVRDUDE_CONF = path.join(ARDUINO_HOME, 'hardware/tools/avr/etc/avrdude.conf') else: AVRDUDE_CONF = path.join(AVR_HOME_DUDE, 'avrdude.conf') AVR_BIN_PREFIX = path.join(AVR_HOME, 'avr-') ARDUINO_LIBS = [path.join(ARDUINO_HOME, 'hardware/arduino/avr/libraries')] if os.path.exists('libs') and os.path.isdir('libs'): ARDUINO_LIBS.append(os.path.abspath('libs')) 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 .ino (or .pde) # 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", "standard")) 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 does not support '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 = path.join('build','core') 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 and os.path.basename(root) == result.group(1): libCandidates.append(path.basename(root)) 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(.ino) 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) #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' def shellquote(s): return "'" + s.replace("'", "'\\''") + "'" avrdudeOpts = ['-V', '-F', '-c %s' % UPLOAD_PROTOCOL, '-b %s' % UPLOAD_SPEED, '-p %s' % MCU, '-P %s' % shellquote(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) # Clean build directory envArduino.Clean('all', 'build/') # vim: et sw=4 fenc=utf-8: