Advanced Scripting

Warning

Advanced Scripting is recommended for Advanced Users and requires Python language knowledge.

We highly recommend to take a look at Dynamic build flags option where you can use any programming language. Also, this option is useful if you need to apply changes to the project before building/uploading process:

  • Macro with the latest VCS revision/tag “on-the-fly”

  • Generate dynamic headers (*.h)

  • Process media content before generating SPIFFS image

  • Make some changes to source code or related libraries

More details Dynamic build flags.

PlatformIO Build System allows one to extend build process with the custom extra_scripts using Python interpreter and SCons construction tool. Build and upload flags, targets, toolchains data, and other information are stored in SCons Construction Environments.

Warning

You can not run/debug these scripts directly with Python interpreter. They will be loaded automatically when you processing project environment using platformio run command.

Launch types

There are 2 launch type of extra scripts:

  1. PRE - executes before a main script of Development Platforms

  2. POST - executes after a main script of Development Platforms

Multiple extra scripts are allowed. Please split them via “, ” (comma + space) in the same line or use multi-line values.

For example, “platformio.ini” (Project Configuration File)

[env:my_env_1]
platform = ...
; without prefix, POST script
extra_scripts = post_extra_script.py

[env:my_env_2]
platform = ...
extra_scripts =
  pre:pre_extra_script.py
  post:post_extra_script1.py
  post_extra_script2.py

This option can also be set by global environment variable PLATFORMIO_EXTRA_SCRIPTS.

Construction Environments

There are 2 built-in construction environments which PlatformIO Build System uses to process a project:

Warning

  1. projenv is available only for POST-type scripts

  2. Flags passed to env using PRE-type script will affect projenv too.

my_pre_extra_script.py:

Import("env")

# access to global construction environment
print(env)

# Dump construction environment (for debug purpose)
print(env.Dump())

# append extra flags to global build environment
# which later will be used to build:
# - project source code
# - frameworks
# - dependent libraries
env.Append(CPPDEFINES=[
  "MACRO_1_NAME",
  ("MACRO_2_NAME", "MACRO_2_VALUE")
])

my_post_extra_script.py:

Import("env", "projenv")

# access to global construction environment
print(env)

# access to project construction environment
print(projenv)

# Dump construction environments (for debug purpose)
print(env.Dump())
print(projenv.Dump())

# append extra flags to global build environment
# which later will be used to build:
# - frameworks
# - dependent libraries
env.Append(CPPDEFINES=[
  "MACRO_1_NAME",
  ("MACRO_2_NAME", "MACRO_2_VALUE")
])

# append extra flags to only project build environment
projenv.Append(CPPDEFINES=[
  "PROJECT_EXTRA_MACRO_1_NAME",
  ("ROJECT_EXTRA_MACRO_2_NAME", "ROJECT_EXTRA_MACRO_2_VALUE")
])

See examples below how to import construction environments and modify existing data or add new.

Before/Pre and After/Post actions

PlatformIO Build System has rich API that allows one to attach different pre-/post actions (hooks) using env.AddPreAction(target, callback) or env.AddPreAction(target, [callback1, callback2, ...]) function. A first argument target can be a name of target that is passed using platformio run --target command, a name of built-in targets (buildprog, size, upload, program, buildfs, uploadfs, uploadfsota) or path to file which PlatformIO processes (ELF, HEX, BIN, OBJ, etc.).

Examples

extra_script.py file is located on the same level as platformio.ini.

platformio.ini:

[env:pre_and_post_hooks]
extra_scripts = post:extra_script.py

extra_script.py:

Import("env", "projenv")

# access to global build environment
print(env)

# access to project build environment (is used source files in "src" folder)
print(projenv)

#
# Dump build environment (for debug purpose)
# print(env.Dump())
#

#
# Change build flags in runtime
#
env.ProcessUnFlags("-DVECT_TAB_ADDR")
env.Append(CPPDEFINES=("VECT_TAB_ADDR", 0x123456789))

#
# Upload actions
#

def before_upload(source, target, env):
    print("before_upload")
    # do some actions

    # call Node.JS or other script
    env.Execute("node --version")


def after_upload(source, target, env):
    print("after_upload")
    # do some actions

print("Current build targets", map(str, BUILD_TARGETS))

env.AddPreAction("upload", before_upload)
env.AddPostAction("upload", after_upload)

#
# Custom actions when building program/firmware
#

env.AddPreAction("buildprog", callback...)
env.AddPostAction("buildprog", callback...)

#
# Custom actions for specific files/objects
#

env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", [callback1, callback2,...])
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", callback...)

# custom action before building SPIFFS image. For example, compress HTML, etc.
env.AddPreAction("$BUILD_DIR/spiffs.bin", callback...)

# custom action for project's main.cpp
env.AddPostAction("$BUILD_DIR/src/main.cpp.o", callback...)

# Custom HEX from ELF
env.AddPostAction(
    "$BUILD_DIR/${PROGNAME}.elf",
    env.VerboseAction(" ".join([
        "$OBJCOPY", "-O", "ihex", "-R", ".eeprom",
        "$BUILD_DIR/${PROGNAME}.elf", "$BUILD_DIR/${PROGNAME}.hex"
    ]), "Building $BUILD_DIR/${PROGNAME}.hex")
)

Custom target

There is a list with built-in targets which could be processed using platformio run --target option. You can create unlimited number of the own targets and declare custom handlers for them.

We will use SCons’s Alias(alias, [targets, [action]]) , env.Alias(alias, [targets, [action]]) function to declare a custom target/alias.

Command shortcut

Create a custom node target (alias) which will print a NodeJS version

platformio.ini:

[env:myenv]
platform = ...
...
extra_scripts = extra_script.py

extra_script.py:

Import("env")
env.AlwaysBuild(env.Alias("node", None, ["node --version"]))

Now, run pio run -t node.

Dependent target

Sometimes you need to run a command which depends on another target (file, firmware, etc). Let’s create an ota target and declare command which will depend on a project firmware. If a build process successes, declared command will be run.

platformio.ini:

[env:myenv]
platform = ...
...
extra_scripts = extra_script.py

extra_script.py:

Import("env")
env.AlwaysBuild(env.Alias("ota",
    "$BUILD_DIR/${PROGNAME}.elf",
    ["ota_script --firmware-path $SOURCE"]))

Now, run pio run -t ota.

Target with options

Let’s create a simple ping target and process it with platformio run --target ping command:

platformio.ini:

[env:env_custom_target]
platform = ...
...
extra_scripts = extra_script.py
custom_ping_host = google.com

extra_script.py:

try:
    import configparser
except ImportError:
    import ConfigParser as configparser

Import("env")

config = configparser.ConfigParser()
config.read("platformio.ini")
host = config.get("env_custom_target", "custom_ping_host")

def mytarget_callback(*args, **kwargs):
    print("Hello PlatformIO!")
    env.Execute("ping " + host)


env.AlwaysBuild(env.Alias("ping", None, mytarget_callback))

Examples

The beast examples are PlatformIO development platforms. Please check builder folder for the main and framework scripts.

Custom options in platformio.ini

platformio.ini:

[env:my_env]
platform = ...
extra_scripts = extra_script.py

custom_option1 = value1
custom_option2 = value2

extra_script.py:

try:
    import configparser
except ImportError:
    import ConfigParser as configparser

config = configparser.ConfigParser()
config.read("platformio.ini")

value1 = config.get("my_env", "custom_option1")
value2 = config.get("my_env", "custom_option2")

Split C/C++ build flags

platformio.ini:

[env:my_env]
platform = ...
extra_scripts = extra_script.py

extra_script.py (place it near platformio.ini):

Import("env")

# General options that are passed to the C and C++ compilers
env.Append(CCFLAGS=["flag1", "flag2"])

# General options that are passed to the C compiler (C only; not C++).
env.Append(CFLAGS=["flag1", "flag2"])

# General options that are passed to the C++ compiler
env.Append(CXXFLAGS=["flag1", "flag2"])

Extra Linker Flags without -Wl, prefix

Sometimes you need to pass extra flags to GCC linker without Wl,. You could use build_flags option but it will not work. PlatformIO will not parse these flags to LINKFLAGS scope. In this case, simple extra script will help:

platformio.ini:

[env:env_extra_link_flags]
platform = windows_x86
extra_scripts = extra_script.py

extra_script.py (place it near platformio.ini):

Import("env")

#
# Dump build environment (for debug)
# print(env.Dump())
#

env.Append(
  LINKFLAGS=[
      "-static",
      "-static-libgcc",
      "-static-libstdc++"
  ]
)

Custom upload tool

You can override default upload command of development platform using extra script. There is the common environment variable UPLOADCMD which PlatformIO Build System will handle when you platformio run -t upload.

Please note that some development platforms can have more than 1 upload command. For example, Atmel AVR has UPLOADHEXCMD (firmware) and UPLOADEEPCMD (EEPROM data).

See examples below:

Template

platformio.ini:

[env:my_custom_upload_tool]
platform = ...
; place it into the root of project or use full path
extra_scripts = extra_script.py
upload_protocol = custom
; each flag in a new line
upload_flags =
  -arg1
  -arg2
  -argN

extra_script.py (place it near platformio.ini):

Import("env")

# please keep $SOURCE variable, it will be replaced with a path to firmware

# Generic
env.Replace(
    UPLOADER="executable or path to executable",
    UPLOADCMD="$UPLOADER $UPLOADERFLAGS $SOURCE"
)

# In-line command with arguments
env.Replace(
    UPLOADCMD="executable -arg1 -arg2 $SOURCE"
)

# Python callback
def on_upload(source, target, env):
    print(source, target)
    firmware_path = str(source[0])
    # do something
    env.Execute("executable arg1 arg2")

env.Replace(UPLOADCMD=on_upload)

Custom openOCD command

platformio.ini:

[env:disco_f407vg]
platform = ststm32
board = disco_f407vg
framework = mbed

extra_scripts = extra_script.py
upload_protocol = custom
; each flag in a new line
upload_flags =
    -f
    scripts/interface/stlink.cfg
    -f
    scripts/target/stm32f4x.cfg

extra_script.py (place it near platformio.ini):

Import("env")

platform = env.PioPlatform()

env.Prepend(
    UPLOADERFLAGS=["-s", platform.get_package_dir("tool-openocd") or ""]
)
env.Append(
    UPLOADERFLAGS=["-c", "program {{$SOURCE}} verify reset; shutdown"]
)
env.Replace(
    UPLOADER="openocd",
    UPLOADCMD="$UPLOADER $UPLOADERFLAGS"
)

Upload to Cloud (OTA)

See project example https://github.com/platformio/bintray-secure-ota

Custom firmware/program name

Sometimes is useful to have a different firmware/program name in build_dir.

platformio.ini:

[env:env_custom_prog_name]
platform = espressif8266
board = nodemcuv2
framework = arduino
build_flags = -D VERSION=13
extra_scripts = pre:extra_script.py

extra_script.py:

Import("env")

my_flags = env.ParseFlags(env['BUILD_FLAGS'])
defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")}
# print(defines)

env.Replace(PROGNAME="firmware_%s" % defines.get("VERSION"))

Override package files

PlatformIO Package Manager automatically installs pre-built packages (Frameworks, toolchains, libraries) required by development Development Platforms and build process. Sometimes you need to override original files with own versions: configure custom GPIO, do changes to built-in LD scripts, or some patching to installed library dependency.

The simplest way is using Diff and Patch technique. How does it work?

  1. Modify original source files

  2. Generate patches

  3. Apply patches via PlatformIO extra script before build process.

Example

We need to patch the original standard/pins_arduino.h variant from Arduino framework and add extra macro #define PIN_A8   (99). Let’s duplicate standard/pins_arduino.h and apply changes. Generate a patch file and place it into patches folder located in the root of a project:

diff ~/.platformio/packages/framework-arduinoavr/variants/standard/pins_arduino.h /tmp/pins_arduino_modified.h > /path/to/platformio/project/patches/1-framework-arduinoavr-add-pin-a8.patch

The result of 1-framework-arduinoavr-add-pin-a8.patch:

63a64
> #define PIN_A8   (99)
112c113
< // 14-21 PA0-PA7 works
---
> // 14-21 PA0-PA7 works

Using extra scripting we can apply patching before a build process. The final result of “platformio.ini” (Project Configuration File) and “PRE” extra script named apply_patches.py:

platformio.ini:

[env:uno]
platform = atmelavr
board = uno
framework = arduino
extra_scripts = pre:apply_patches.py

apply_patches.py:

from os.path import join, isfile

Import("env")

FRAMEWORK_DIR = env.PioPlatform().get_package_dir("framework-arduinoavr")
patchflag_path = join(FRAMEWORK_DIR, ".patching-done")

# skip patch process if we did it before
if isfile(join(FRAMEWORK_DIR, ".patching-done")):
    env.Exit(0)

original_file = join(FRAMEWORK_DIR, "variants", "standard", "pins_arduino.h")
patched_file = join("patches", "1-framework-arduinoavr-add-pin-a8.patch")

assert isfile(original_file) and isfile(patched_file)

env.Execute("patch %s %s" % (original_file, patched_file))
# env.Execute("touch " + patchflag_path)


def _touch(path):
    with open(path, "w") as fp:
        fp.write("")

env.Execute(lambda *args, **kwargs: _touch(patchflag_path))

Please note that this example will work on a system where a patch tool is available. For Windows OS, you can use patch and diff tools provided by Git client utility (located inside installation directory).

If you need to make it more independent to the operating system, please replace the patch with a multi-platform python-patch script.

Override Board Configuration

PlatformIO allows to override some basic options (integer or string values) using More options in “platformio.ini” (Project Configuration File). Sometimes you need to do complex changes to default board manifest and extra PRE scripting work well here. See example below how to override default hardware VID/PIDs:

platformio.ini:

[env:uno]
platform = atmelavr
board = uno
framework = arduino
extra_scripts = pre:custon_hwids.py

custon_hwids.py:

Import("env")

board_config = env.BoardConfig()
board_config.update("build.hwids", [
  ["0x2341", "0x0243"],
  ["0x2A03", "0x0043"]
])