|
| 1 | +# Adafruit Arduino Board Package Tool (bpt) |
| 2 | +# Swiss Army knife for managing Arduino board packages. Can check board packages |
| 3 | +# against a published board package index and notify when newer versions are |
| 4 | +# available, automatically build updated packages for the index, and more. |
| 5 | +# Author: Tony DiCola |
| 6 | +# |
| 7 | +# Copyright (c) 2016 Adafruit Industries |
| 8 | +# |
| 9 | +# Permission is hereby granted, free of charge, to any person obtaining a copy of |
| 10 | +# this software and associated documentation files (the "Software"), to deal in |
| 11 | +# the Software without restriction, including without limitation the rights to |
| 12 | +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
| 13 | +# the Software, and to permit persons to whom the Software is furnished to do so, |
| 14 | +# subject to the following conditions: |
| 15 | +# |
| 16 | +# The above copyright notice and this permission notice shall be included in all |
| 17 | +# copies or substantial portions of the Software. |
| 18 | +# |
| 19 | +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 20 | +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
| 21 | +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
| 22 | +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
| 23 | +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
| 24 | +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 25 | +import json |
| 26 | +import SimpleHTTPServer |
| 27 | +import SocketServer |
| 28 | + |
| 29 | +from bpt_model import * |
| 30 | +import click |
| 31 | +from git import Repo |
| 32 | +from pkg_resources import parse_version |
| 33 | + |
| 34 | + |
| 35 | +logger = logging.getLogger(__name__) |
| 36 | + |
| 37 | + |
| 38 | +class BptContext(object): |
| 39 | + """Context object which holds global state passed between click groups and |
| 40 | + commands. |
| 41 | + """ |
| 42 | + |
| 43 | + def __init__(self): |
| 44 | + self.board_config_file = None |
| 45 | + self.board_index_file = None |
| 46 | + self.board_packages = None |
| 47 | + self.board_config = None |
| 48 | + self.board_index = None |
| 49 | + |
| 50 | + def load_data(self): |
| 51 | + """Load all the package and package index metadata to prepare for |
| 52 | + processing. This will read the package config INI file and all grab all |
| 53 | + the packages it mentions, then load the package index JSON and parse it. |
| 54 | + """ |
| 55 | + click.echo('Loading current packages from their origin repository/directory...') |
| 56 | + # Load the board configuration file. |
| 57 | + self.board_config = BoardConfig(self.board_config_file) |
| 58 | + # Save the board packages in the context so other commands can read and |
| 59 | + # process them. |
| 60 | + self.board_packages = self.board_config.get_packages() |
| 61 | + # Now read in the board index JSON file and parse it, then save in global context. |
| 62 | + with open(self.board_index_file, 'r') as bi: |
| 63 | + self.board_index = BoardIndex(json.load(bi)) |
| 64 | + |
| 65 | + |
| 66 | +@click.group() |
| 67 | +@click.option('--debug', '-d', is_flag=True, |
| 68 | + help='Enable debug output.') |
| 69 | +@click.option('--board-config', '-c', default='bpt.ini', |
| 70 | + type=click.Path(dir_okay=False), |
| 71 | + help='Specify a INI config file with list of board packages to use. Default is a bpt.ini in the current directory.') |
| 72 | +@click.option('--board-index', '-i', default='package_mysensors.org_index.json', |
| 73 | + type=click.Path(exists=True, dir_okay=False), |
| 74 | + help='Specify a board index JSON file. This is the master index that publishes all the packages.') |
| 75 | +@click.pass_context |
| 76 | +def bpt_command(ctx, debug, board_config, board_index): |
| 77 | + """Adafruit Arduino Board Package Tool (bpt) |
| 78 | +
|
| 79 | + Swiss Army knife for managing Arduino board packages. Can check board packages |
| 80 | + against a published board package index and notify when newer versions are |
| 81 | + available, automatically build updated packages for the index, and more. |
| 82 | + """ |
| 83 | + if debug: |
| 84 | + logging.basicConfig(level=logging.DEBUG) |
| 85 | + ctx.obj.board_config_file = board_config |
| 86 | + ctx.obj.board_index_file = board_index |
| 87 | + |
| 88 | + |
| 89 | +@bpt_command.command() |
| 90 | +@click.pass_context |
| 91 | +def check_updates(ctx): |
| 92 | + """Check package index for out of date packages. |
| 93 | +
|
| 94 | + Scan a set of board packages and compare their version against the most |
| 95 | + recent version published in a board index. Will alert of any board packages |
| 96 | + which have a newer version than is published in the board index. |
| 97 | + """ |
| 98 | + ctx.obj.load_data() # Load all the package config & metadata. |
| 99 | + click.echo('Found the following current packages:') |
| 100 | + for package in ctx.obj.board_packages: |
| 101 | + click.echo('- {0}'.format(package.get_name())) |
| 102 | + click.echo(' version = {0}'.format(package.get_version())) |
| 103 | + click.echo(' origin = {0}'.format(package.get_origin())) |
| 104 | + # Go through each board package loaded from the board package config INI |
| 105 | + # and check if its version is newer than the related packages in the board |
| 106 | + # index. |
| 107 | + click.echo('Comparing current packages with published versions in board index...') |
| 108 | + for package in ctx.obj.board_packages: |
| 109 | + parent = package.get_parent() |
| 110 | + name = package.get_name() |
| 111 | + version = package.get_version() |
| 112 | + click.echo('- {0}'.format(name)) |
| 113 | + # Get all the associated packages in the board index. |
| 114 | + index_packages = ctx.obj.board_index.get_platforms(parent, name) |
| 115 | + # Skip to the next package if nothing was found in the board index for this package. |
| 116 | + if len(index_packages) == 0: |
| 117 | + click.echo(' Not found in board index!') |
| 118 | + continue |
| 119 | + # Find the most recent version in the index packages. |
| 120 | + latest = max(map(lambda x: parse_version(x.get('version', '')), index_packages)) |
| 121 | + click.echo(' latest index version = {0}'.format(str(latest))) |
| 122 | + # Warn if the latest published package is older than the current package |
| 123 | + # from its origin source. |
| 124 | + if latest < parse_version(version): |
| 125 | + click.echo(' !!!! BOARD INDEX NOT UP TO DATE !!!!') |
| 126 | + |
| 127 | + |
| 128 | +@bpt_command.command() |
| 129 | +@click.option('--force', '-f', is_flag=True, |
| 130 | + help='Force the specified package to be updated even if the version is older than currently in the index.') |
| 131 | +@click.option('--output-board-index', '-o', |
| 132 | + type=click.Path(dir_okay=False, writable=True), |
| 133 | + help='Specify the new board index JSON file to write. If not specified the input board index (--board-index value or its default) will be used.') |
| 134 | +@click.option('--output-board-dir', '-od', default='boards', |
| 135 | + type=click.Path(file_okay=False, writable=True), |
| 136 | + help="Specify the directory to write the board package archive file. Default is a 'boards' subdirectory in the current location.") |
| 137 | +@click.argument('package_name') |
| 138 | +@click.pass_context |
| 139 | +def update_index(ctx, package_name, force, output_board_index, output_board_dir): |
| 140 | + """Update board package in the published index. |
| 141 | +
|
| 142 | + This command will archive and compress a board package and add it to the |
| 143 | + board index file. A sanity check will be done to ensure the package has a |
| 144 | + later version than currently in the board index, however this can be disabled |
| 145 | + with the --force option. |
| 146 | +
|
| 147 | + The command takes one argument, the name of the board package to update. |
| 148 | + This should be the name of the package as defined in the board package config |
| 149 | + INI file section name (use the check_updates command to list all the packages |
| 150 | + from the config if unsure). |
| 151 | + """ |
| 152 | + ctx.obj.load_data() # Load all the package config & metadata. |
| 153 | + # Use the input board index as the output if none is specified. |
| 154 | + if output_board_index is None: |
| 155 | + output_board_index = ctx.obj.board_index_file |
| 156 | + # Validate that the specified package exists in the config. |
| 157 | + package = ctx.obj.board_config.get_package(package_name) |
| 158 | + if package is None: |
| 159 | + raise click.BadParameter('Could not find specified package in the board package config INI file! Run check_updates command to list all configured package names.', |
| 160 | + param_hint='package') |
| 161 | + # If not in force mode do a sanity check to make sure the package source |
| 162 | + # has a newer version than in the index. |
| 163 | + if not force: |
| 164 | + # Get all the associated packages in the board index. |
| 165 | + index_packages = ctx.obj.board_index.get_platforms(package.get_parent(), |
| 166 | + package.get_name()) |
| 167 | + # Do version check if packages were found in the index. |
| 168 | + if len(index_packages) > 0: |
| 169 | + # Find the most recent version in the index packages. |
| 170 | + latest = max(map(lambda x: parse_version(x.get('version', '')), index_packages)) |
| 171 | + # Warn if the latest published package is the same or newer than the |
| 172 | + # current package from its origin source. |
| 173 | + if latest >= parse_version(package.get_version()): |
| 174 | + raise click.UsageError('Specified package is older than the version currently in the index! Use the --force option to force this update if necessary.') |
| 175 | + # Create the output directory if it doesn't exist. |
| 176 | + if not os.path.exists(output_board_dir): |
| 177 | + os.makedirs(output_board_dir) |
| 178 | + # Build the archive with the board package data and write it to the target |
| 179 | + # directory. |
| 180 | + archive_path = os.path.join(output_board_dir, package.get_archive_name()) |
| 181 | + size, sha256 = package.write_archive(archive_path) |
| 182 | + click.echo('Created board package archive: {0}'.format(archive_path)) |
| 183 | + # Convert the package template from JSON to a platform metadata dict that |
| 184 | + # can be inserted in the board index. |
| 185 | + template_params = { |
| 186 | + 'version': package.get_version(), |
| 187 | + 'filename': package.get_archive_name(), |
| 188 | + 'sha256': sha256, |
| 189 | + 'size': size |
| 190 | + } |
| 191 | + platform = json.loads(package.get_template().format(**template_params)) |
| 192 | + # Add the new pacakge metadata to the board index. |
| 193 | + ctx.obj.board_index.add_platform(package.get_parent(), platform) |
| 194 | + # Write out the new board index JSON. |
| 195 | + new_index = ctx.obj.board_index.write_json() |
| 196 | + with open(output_board_index, 'w') as bi: |
| 197 | + bi.write(new_index) |
| 198 | + click.echo('Wrote updated board index JSON: {0}'.format(output_board_index)) |
| 199 | + |
| 200 | + |
| 201 | +@bpt_command.command() |
| 202 | +@click.option('--url-transform', '-u', default='adafruit.github.io/arduino-board-index', |
| 203 | + help='URL domain and starting path to replace with the localhost:<port> test server in the index during testing. Must be set or else the index will reference files on the internet, not local machine!') |
| 204 | +@click.option('--port', '-p', type=click.INT, default=8000, |
| 205 | + help='Port number to use for the test server.') |
| 206 | +@click.pass_context |
| 207 | +def test_server(ctx, url_transform, port): |
| 208 | + """Run a local webserver to test board index. |
| 209 | +
|
| 210 | + Create a local webserver on port 8000 (but can be changed with the --port |
| 211 | + option) that will serve the board package index. Setup the Arduino IDE to |
| 212 | + use the board package URL: |
| 213 | + http://localhost:8000/package_test_index.json |
| 214 | + """ |
| 215 | + # TODO: Don't hard code this file name. However Arduino _must_ see a file |
| 216 | + # named 'package_<PACKAGE_NAME>_index.json' for the file to work, and we |
| 217 | + # don't want to modify the real index with the transformations below. for |
| 218 | + # now we just use a test package file. |
| 219 | + test_index = 'package_test_index.json' |
| 220 | + ctx.obj.load_data() # Load all the package config & metadata. |
| 221 | + # Start the webserver from inside the directory with the index file. |
| 222 | + index_dir, index_filename = os.path.split(ctx.obj.board_index_file) |
| 223 | + if index_dir is not None and index_dir != '': |
| 224 | + os.chdir(index_dir) |
| 225 | + # Transform all package url values in the index to use http instead of https |
| 226 | + # (SSL is unsupported by Python's simple web server), and to replace the |
| 227 | + # domain and root of the URL with the localhost:<port> value so the data |
| 228 | + # is served locally instead of from the remote server. |
| 229 | + ctx.obj.board_index.transform_urls([ |
| 230 | + ('https://', 'http://'), # Replace https:// with http:// |
| 231 | + (url_transform, 'localhost:{0}'.format(port)) # Replace remote server URL with local test server. |
| 232 | + ]) |
| 233 | + # Write out the test board index JSON. |
| 234 | + with open(test_index, 'w') as bi: |
| 235 | + bi.write(ctx.obj.board_index.write_json()) |
| 236 | + try: |
| 237 | + server = SocketServer.TCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler) |
| 238 | + click.echo('Source board index file: {0}'.format(ctx.obj.board_index_file)) |
| 239 | + click.echo('Test server listening at: http://localhost:{0}'.format(port)) |
| 240 | + click.echo('Configure Arduino to use the following board package URL:') |
| 241 | + click.echo(' http://localhost:{0}/{1}'.format(port, test_index)) |
| 242 | + server.serve_forever() |
| 243 | + finally: |
| 244 | + # Cleanup the test index file that was created. |
| 245 | + os.remove(test_index) |
| 246 | + |
| 247 | + |
| 248 | +if __name__ == '__main__': |
| 249 | + try: |
| 250 | + # Create a board package tool context object that will hold all global |
| 251 | + # state passed between command handler functions. |
| 252 | + context = BptContext() |
| 253 | + # Invoke click command processing. |
| 254 | + bpt_command(obj=context) |
| 255 | + finally: |
| 256 | + # Close any board packages that were opened, this will clean up temporary |
| 257 | + # file locations, etc. |
| 258 | + if context.board_packages is not None: |
| 259 | + for package in context.board_packages: |
| 260 | + package.close() |
0 commit comments