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