ci-arduino/build_platform.py

454 lines
17 KiB
Python
Executable file

import sys
import glob
import time
import os
import re
import shutil
import subprocess
import collections
from contextlib import contextmanager
from all_platforms import ALL_PLATFORMS
# optional wall option cause build failed if has warnings
BUILD_WALL = False
BUILD_WARN = True
if "--wall" in sys.argv:
BUILD_WALL = True
sys.argv.remove("--wall")
if "--no_warn" in sys.argv:
BUILD_WARN = False
sys.argv.remove("--no_warn")
# optional timeout argument to extend build time
# for larger sketches or firmware builds
BUILD_TIMEOUT = False
if "--build_timeout" in sys.argv:
BUILD_TIMEOUT = True
popen_timeout = int(sys.argv[sys.argv.index("--build_timeout") + 1])
sys.argv.pop(sys.argv.index("--build_timeout") + 1)
sys.argv.remove("--build_timeout")
# add user bin to path!
BUILD_DIR = ''
# add user bin to path!
try:
# If we're on actions
BUILD_DIR = os.environ["GITHUB_WORKSPACE"]
except KeyError:
try:
# If we're on travis
BUILD_DIR = os.environ["TRAVIS_BUILD_DIR"]
except KeyError:
# If we're running on local machine
BUILD_DIR = os.path.abspath(".")
pass
os.environ["PATH"] += os.pathsep + BUILD_DIR + "/bin"
print("build dir:", BUILD_DIR)
IS_LEARNING_SYS = False
if "Adafruit_Learning_System_Guides" in BUILD_DIR:
print("Found learning system repo")
IS_LEARNING_SYS = True
shutil.rmtree(BUILD_DIR + "/ci/examples/Blink")
elif "METROX-Examples-and-Project-Sketches" in BUILD_DIR:
print("Found MetroX Examples Repo")
IS_LEARNING_SYS = True
#os.system('pwd')
#os.system('ls -lA')
CROSS = u'\N{cross mark}'
CHECK = u'\N{check mark}'
BSP_URLS = "https://adafruit.github.io/arduino-board-index/package_adafruit_index.json,http://arduino.esp8266.com/stable/package_esp8266com_index.json,https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json,https://sandeepmistry.github.io/arduino-nRF5/package_nRF5_boards_index.json,https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json,https://drazzy.good-enough.cloud/package_drazzy.com_index.json"
class ColorPrint:
@staticmethod
def print_fail(message, end = '\n'):
sys.stdout.write('\x1b[1;31m' + message.strip() + '\x1b[0m' + end)
@staticmethod
def print_pass(message, end = '\n'):
sys.stdout.write('\x1b[1;32m' + message.strip() + '\x1b[0m' + end)
@staticmethod
def print_warn(message, end = '\n'):
sys.stdout.write('\x1b[1;33m' + message.strip() + '\x1b[0m' + end)
@staticmethod
def print_info(message, end = '\n'):
sys.stdout.write('\x1b[1;34m' + message.strip() + '\x1b[0m' + end)
@staticmethod
def print_bold(message, end = '\n'):
sys.stdout.write('\x1b[1;37m' + message.strip() + '\x1b[0m' + end)
def manually_install_esp32_bsp(repo_info):
print("Manually installing latest ESP32 BSP...")
# Assemble git url
repo_url = "git clone -b {0} https://github.com/{1}/arduino-esp32.git esp32".format(repo_info.split("/")[1], repo_info.split("/")[0])
# Locally clone repo (https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html#linux)
os.system("mkdir -p /home/runner/Arduino/hardware/espressif")
print("Cloning %s"%repo_url)
cmd = "cd /home/runner/Arduino/hardware/espressif && " + repo_url
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
r = proc.wait(timeout=1000)
out = proc.stdout.read()
err = proc.stderr.read()
if r != 0:
ColorPrint.print_fail("Failed to download ESP32 Arduino BSP!")
ColorPrint.print_fail(out.decode("utf-8"))
ColorPrint.print_fail(err.decode("utf-8"))
exit(-1)
print(out)
print("Cloned repository!")
print("Installing ESP32 Arduino BSP...")
cmd = "cd /home/runner/Arduino/hardware/espressif/esp32/tools && python3 get.py"
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
r = proc.wait(timeout=1000)
out = proc.stdout.read()
err = proc.stderr.read()
if r != 0:
ColorPrint.print_fail("Failed to install ESP32 Arduino BSP!")
ColorPrint.print_fail(out.decode("utf-8"))
ColorPrint.print_fail(err.decode("utf-8"))
exit(-1)
print(out)
print("Installed ESP32 BSP from source!")
def install_platform(fqbn, full_platform_name=None):
if os.path.exists("/home/runner/.arduino15/package_drazzy.json"):
print("Moving drazzy.json")
shutil.move("/home/runner/.arduino15/package_drazzy.json", "/home/runner/.arduino15/package_drazzy.com_index.json")
print("Installing", fqbn, end=" ")
if fqbn == "adafruit:avr": # we have a platform dep
install_platform("arduino:avr", full_platform_name)
if full_platform_name[2] is not None:
manually_install_esp32_bsp(full_platform_name[2]) # build esp32 bsp from desired source and branch
for retry in range(0, 3):
print("arduino-cli core install "+fqbn+" --additional-urls "+BSP_URLS)
if os.system("arduino-cli core install "+fqbn+" --additional-urls "+BSP_URLS+" > /dev/null") == 0:
break
print("...retrying...", end=" ")
time.sleep(10) # wait 10 seconds then try again?
else:
# tried 3 times to no avail
ColorPrint.print_fail("FAILED to install "+fqbn)
exit(-1)
ColorPrint.print_pass(CHECK)
# print installed core version
print(os.popen('arduino-cli core list | grep {}'.format(fqbn)).read(), end='')
def run_or_die(cmd, error):
print(cmd)
attempt = 0
while attempt < 3:
if os.system(cmd) == 0:
return
attempt += 1
print('attempt {} failed, {} retry left'.format(attempt, 3-attempt))
time.sleep(5)
ColorPrint.print_fail(error)
exit(-1)
################################ Install Arduino IDE
print()
ColorPrint.print_info('#'*40)
print("INSTALLING ARDUINO BOARDS")
ColorPrint.print_info('#'*40)
run_or_die("arduino-cli core update-index --additional-urls "+BSP_URLS+
" > /dev/null", "FAILED to update core indices")
print()
def is_library_installed(lib_name):
try:
installed_libs = subprocess.check_output(["arduino-cli", "lib", "list"]).decode("utf-8")
return not all(not item for item in [re.match('^'+dep+'\\s*\\d+\\.', line) for line in installed_libs.split('\n')])
except subprocess.CalledProcessError as e:
print("Error checking installed libraries:", e)
return False
################################ Install dependencies
our_name=None
try:
if IS_LEARNING_SYS:
libprop = open(BUILD_DIR+'/library.deps')
else:
libprop = open(BUILD_DIR+'/library.properties')
for line in libprop:
if line.startswith("name="):
our_name = line.replace("name=", "").strip()
if line.startswith("depends="):
deps = line.replace("depends=", "").split(",")
for dep in deps:
dep = dep.strip()
if not is_library_installed(dep):
print("Installing "+dep)
run_or_die('arduino-cli lib install "'+dep+'" > /dev/null',
"FAILED to install dependency "+dep)
else:
print("Skipping already installed lib: "+dep)
except OSError:
print("No library dep or properties found!")
pass # no library properties
# Delete the existing library if we somehow downloaded
# due to dependencies
if our_name:
run_or_die("arduino-cli lib uninstall \""+our_name+"\"", "Could not uninstall")
print("Libraries installed: ", glob.glob(os.environ['HOME']+'/Arduino/libraries/*'))
# link our library folder to the arduino libraries folder
if not IS_LEARNING_SYS:
try:
os.symlink(BUILD_DIR, os.environ['HOME']+'/Arduino/libraries/' + os.path.basename(BUILD_DIR))
except FileExistsError:
pass
################################ UF2 Utils.
def glob01(pattern):
result = glob.glob(pattern)
if len(result) > 1:
raise RuntimeError(f"Required pattern {pattern} to match at most 1 file, got {result}")
return result[0] if result else None
def glob1(pattern):
result = glob.glob(pattern)
if len(result) != 1:
raise RuntimeError(f"Required pattern {pattern} to match exactly 1 file, got {result}")
return result[0]
def download_uf2_utils():
"""Downloads uf2conv tools if we don't already have them
"""
cmd = "wget -nc --no-check-certificate http://raw.githubusercontent.com/microsoft/uf2/master/utils/uf2families.json https://raw.githubusercontent.com/microsoft/uf2/master/utils/uf2conv.py"
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
r = proc.wait(timeout=60)
out = proc.stdout.read()
err = proc.stderr.read()
if r != 0:
ColorPrint.print_fail("Failed to download UF2 Utils!")
ColorPrint.print_fail(out.decode("utf-8"))
ColorPrint.print_fail(err.decode("utf-8"))
return False
return True
def generate_uf2(example_path):
"""Generates a .uf2 file from a .bin or .hex file.
:param str example_path: A path to the compiled .bin or .hex file.
"""
if not download_uf2_utils():
return None
cli_build_uf2_path = "build/*.*." + fqbn.split(':')[2] + "/*.uf2"
uf2_input_file = glob01(os.path.join(example_path, cli_build_uf2_path))
# Some platforms, like rp2040, directly generate a uf2 file, so no need to do it ourselves
if uf2_input_file is not None:
output_file = os.path.splitext(uf2_input_file)[0] + ".uf2"
ColorPrint.print_pass(CHECK)
ColorPrint.print_info("Used uf2 generated by arduino-cli")
return output_file
# Generate using a hex file for all platforms except for ESP32-S2, ESP32-S3 (exports as .bin files)
if not any (x in fqbn for x in ["esp32s2", "esp32s3"]):
cli_build_hex_path = "build/*.*." + fqbn.split(':')[2] + "/*.hex"
hex_input_file = glob1(os.path.join(example_path, cli_build_hex_path))
output_file = os.path.splitext(hex_input_file)[0] + ".uf2"
family_id = ALL_PLATFORMS[platform][1]
cmd = ['python3', 'uf2conv.py', hex_input_file, '-c', '-f', family_id, '-o', output_file]
else:
cli_build_path = "build/*.*." + fqbn.split(':')[2] + "/*.ino.bin"
input_file = glob1(os.path.join(example_path, cli_build_path))
output_file = os.path.splitext(input_file)[0] + ".uf2"
family_id = ALL_PLATFORMS[platform][1]
cmd = ['python3', 'uf2conv.py', input_file, '-c', '-f', family_id, '-b', "0x0000", '-o', output_file]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if BUILD_TIMEOUT:
r = proc.wait(timeout=popen_timeout)
else:
r = proc.wait(timeout=60)
out = proc.stdout.read()
err = proc.stderr.read()
if r == 0: # and not err: # we might get warnings that do not affect the result
ColorPrint.print_pass(CHECK)
ColorPrint.print_info(out.decode("utf-8"))
else:
ColorPrint.print_fail(CROSS)
ColorPrint.print_fail("\n\rERRCODE:", str(r))
ColorPrint.print_fail("\n\rOUTPUT: ", out.decode("utf-8"))
ColorPrint.print_fail("\n\rERROR: ", err.decode("utf-8"))
return None
return output_file
################################ Test platforms
platforms = []
success = 0
# expand groups:
for arg in sys.argv[1:]:
platform = ALL_PLATFORMS.get(arg, None)
if isinstance(platform, list):
platforms.append(arg)
elif isinstance(platform, tuple):
for p in platform:
platforms.append(p)
else:
print("Unknown platform: ", arg)
exit(-1)
@contextmanager
def group_output(title):
sys.stdout.flush()
sys.stderr.flush()
print(f"::group::{title}")
try:
yield
finally:
sys.stdout.flush()
sys.stderr.flush()
print(f"::endgroup::")
sys.stdout.flush()
def test_examples_in_folder(folderpath):
global success
for example in sorted(os.listdir(folderpath)):
examplepath = folderpath+"/"+example
if os.path.isdir(examplepath):
test_examples_in_folder(examplepath)
continue
if not examplepath.endswith(".ino"):
continue
print('\t'+example, end=' ')
# check if we should SKIP
skipfilename = folderpath+"/."+platform+".test.skip"
onlyfilename = folderpath+"/."+platform+".test.only"
# check if we should GENERATE UF2
gen_file_name = folderpath+"/."+platform+".generate"
if os.path.exists(skipfilename):
ColorPrint.print_warn("skipping")
continue
if glob.glob(folderpath+"/.*.test.only"):
platformname = glob.glob(folderpath+"/.*.test.only")[0].split('.')[1]
if platformname != "none" and not platformname in ALL_PLATFORMS:
# uh oh, this isnt a valid testonly!
ColorPrint.print_fail(CROSS)
ColorPrint.print_fail("This example does not have a valid .platform.test.only file")
success = 1
continue
if not os.path.exists(onlyfilename):
ColorPrint.print_warn("skipping")
continue
if os.path.exists(gen_file_name):
ColorPrint.print_info("generating")
if BUILD_WARN:
if os.path.exists(gen_file_name):
cmd = ['arduino-cli', 'compile', '--warnings', 'all', '--fqbn', fqbn, '-e', folderpath]
else:
cmd = ['arduino-cli', 'compile', '--warnings', 'all', '--fqbn', fqbn, folderpath]
else:
cmd = ['arduino-cli', 'compile', '--warnings', 'none', '--export-binaries', '--fqbn', fqbn, folderpath]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
if BUILD_TIMEOUT:
out, err = proc.communicate(timeout=popen_timeout)
else:
out, err = proc.communicate(timeout=120)
r = proc.returncode
except:
proc.kill()
out, err = proc.communicate()
r = 1
if r == 0 and not (err and BUILD_WALL == True):
ColorPrint.print_pass(CHECK)
if err:
# also print out warning message
with group_output(f"{example} {fqbn} build output"):
ColorPrint.print_fail(err.decode("utf-8"))
if os.path.exists(gen_file_name):
if ALL_PLATFORMS[platform][1] == None:
ColorPrint.print_info("Platform does not support UF2 files, skipping...")
else:
ColorPrint.print_info("Generating UF2...")
filename = generate_uf2(folderpath)
if filename is None:
success = 1 # failure
if IS_LEARNING_SYS:
fqbnpath, uf2file = filename.split("/")[-2:]
os.makedirs(BUILD_DIR+"/build", exist_ok=True)
os.makedirs(BUILD_DIR+"/build/"+fqbnpath, exist_ok=True)
shutil.copy(filename, BUILD_DIR+"/build/"+fqbnpath+"-"+uf2file)
os.system("ls -lR "+BUILD_DIR+"/build")
else:
ColorPrint.print_fail(CROSS)
with group_output(f"{example} {fqbn} built output"):
ColorPrint.print_fail(out.decode("utf-8"))
ColorPrint.print_fail(err.decode("utf-8"))
success = 1
def test_examples_in_learningrepo(folderpath):
global success
for project in os.listdir(folderpath):
projectpath = folderpath+"/"+project
if os.path.isdir(learningrepo):
test_examples_in_learningrepo(projectpath)
continue
if not projectpath.endswith(".ino"):
continue
# found an INO!
print('\t'+projectpath, end=' ', flush=True)
# check if we should SKIP
skipfilename = folderpath+"/."+platform+".test.skip"
onlyfilename = folderpath+"/."+platform+".test.only"
if os.path.exists(skipfilename):
ColorPrint.print_warn("skipping")
continue
elif glob.glob(folderpath+"/.*.test.only") and not os.path.exists(onlyfilename):
ColorPrint.print_warn("skipping")
continue
cmd = ['arduino-cli', 'compile', '--warnings', 'all', '--fqbn', fqbn, projectpath]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
r = proc.wait()
out = proc.stdout.read()
err = proc.stderr.read()
if r == 0:
ColorPrint.print_pass(CHECK)
if err:
# also print out warning message
ColorPrint.print_fail(err.decode("utf-8"))
else:
ColorPrint.print_fail(CROSS)
ColorPrint.print_fail(out.decode("utf-8"))
ColorPrint.print_fail(err.decode("utf-8"))
success = 1
for platform in platforms:
fqbn = ALL_PLATFORMS[platform][0]
print('#'*80)
ColorPrint.print_info("SWITCHING TO "+fqbn)
install_platform(":".join(fqbn.split(':', 2)[0:2]), ALL_PLATFORMS[platform]) # take only first two elements
print('#'*80)
if not IS_LEARNING_SYS:
test_examples_in_folder(BUILD_DIR+"/examples")
else:
test_examples_in_folder(BUILD_DIR)
exit(success)