Skip to content

Commit ff9f795

Browse files
committed
otamaker: Add a source: key and -C <dir> option
Add an explicit source: key as an alternative to the comment after the name: key as a way to specify the path to the source file. Like a comment, the source: key is dropped from the manifest/contents. If both forms are present, the source: key takes precedence. As before, paths are resolved relative to the current directory, but this can be overridden with -C <dir> (--directory), as with tar. See: #223 Signed-off-by: Phil Elwell <phil@raspberrypi.com>
1 parent 3f5fa29 commit ff9f795

2 files changed

Lines changed: 101 additions & 41 deletions

File tree

‎otamaker/README.md‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,12 @@ artefact:
6161
payloads:
6262
- name: preinstall
6363
type: script
64-
- name: boot # boot.sparse
64+
- name: boot
6565
type: image-sparse
66-
- name: system # system.sparse
66+
source: boot.sparse
67+
- name: system
6768
type: image-sparse
69+
source: system.sparse
6870
- name: installdata.tgz
6971
type: tmpfile
7072
- name: installer
@@ -87,7 +89,7 @@ The following payload types are recognised:
8789
* `script` - a program to execute on the remote device. As the name suggests, this is expected to be a script of some description, but it doesn't have to be.
8890
* `tmpfile` - some other file to be copied to the device for the duration of the update. These are intended to be used by scripts, which they must appear before in the list of payloads in order to be accessible.
8991

90-
Comments immediately after (and on the same line as) the `name` field are interpreted by otamaker as a path to the source file. These comments are removed when the final archive is created.
92+
Each payload is read from the file named by its `source` field, or from the file with the same name as the payload if there is no `source`. Paths are relative to the current directory, unless `-C <dir>` (`--directory`) gives another, as for `tar`. `source` is a build-time field: otamaker removes it from the copy of the contents in the archive. An older form, a comment immediately after (and on the same line as) the `name` field, is still accepted as the path when there is no `source`; such comments are also removed.
9193

9294
**Scripts**
9395

@@ -167,12 +169,14 @@ artefact:
167169
device_type: rpi
168170
169171
payloads:
170-
- name: boot # work/image-myapp-1.0.3/boot.sparse
172+
- name: boot
171173
type: image-sparse
172-
- name: system # work/image-myapp-1.0.3/system.sparse
174+
source: work/image-myapp-1.0.3/boot.sparse
175+
- name: system
173176
type: image-sparse
177+
source: work/image-myapp-1.0.3/system.sparse
174178
```
175-
This artefact YAML file is designed to be used from root of the rpi-image-gen repository - you could also drop the paths, keeping `boot.sparse` and `system.sparse`, and run `otamaker` from within the `work/image-myapp-1.0.3` directory (or your equivalent). Here's the output it generated:
179+
This artefact YAML file is designed to be used from root of the rpi-image-gen repository - you could also drop the paths, keeping `boot.sparse` and `system.sparse`, and either run `otamaker` from within the `work/image-myapp-1.0.3` directory (or your equivalent) or point it there with `-C`. Here's the output it generated:
176180
```
177181
pi@philpi5:~/rpi-image-gen $ otamaker abupdate.yaml
178182
Contents:

‎otamaker/otamaker‎

Lines changed: 91 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
"""
33
Build an OTA artefact from a contents YAML file.
44
Usage: otamaker [<options>] <contents file> [<outfile>]
5+
6+
Each payload is read from the file named by its 'source:' key, or by a comment
7+
on its 'name:' line (the older form), or failing both by its name. Paths are
8+
relative to the current directory, or to -C <dir>. 'source:'
9+
is dropped from the copy of the contents in the artefact.
510
"""
611

712
# Author: Phil Elwell <phil@raspberrypi.com>
@@ -61,6 +66,10 @@ def main():
6166
parser.add_argument('outfile',
6267
help="the artefact name (defaults to the 'name' in the contents)",
6368
nargs='?')
69+
parser.add_argument('-C', '--directory',
70+
help='the directory payload paths are relative to '
71+
'(default: the current directory)',
72+
metavar='DIR')
6473
parser.add_argument('-w', '--writesum',
6574
help='also write the sha256sum to <outfile>.sha256sum',
6675
action='store_true')
@@ -74,13 +83,18 @@ def main():
7483
print(f"* failed to open '{args.contents}'")
7584
sys.exit(1)
7685

77-
cwd = os.getcwd()
86+
basedir = args.directory if args.directory is not None else os.getcwd()
87+
basedir = os.path.abspath(basedir)
88+
if not os.path.isdir(basedir):
89+
print(f"* error: directory '{basedir}' not found")
90+
sys.exit(1)
7891
state = BLOCK_NONE
7992
contents = []
8093
payloads = [CONTENTS_FILE]
8194
outfile = args.outfile
8295
errors = 0
83-
current_payload = {}
96+
groups = [] # One per payload: name, type, source, comment
97+
item_prefix = None # The '- ' of a list item whose first line was dropped
8498

8599
with tempfile.TemporaryDirectory() as tmpdir:
86100
for raw_line in lines:
@@ -92,6 +106,24 @@ def main():
92106
line = m.group(1)
93107
comment = m.group(2).strip()
94108

109+
if state == BLOCK_PAYLOADS:
110+
# A payload's file is given by a 'source:' key, which otamaker
111+
# drops from the embedded copy, or else by a comment on the
112+
# 'name:' line (the older form), or else it's the name itself.
113+
payload_line = re.sub(r'^\s*-\s*', ' ', line)
114+
source_m = re.match(r'^\s+source:\s*(.*)$', payload_line)
115+
if source_m and (groups or re.match(r'^\s*-', line)):
116+
if re.match(r'^\s*-', line):
117+
groups.append({'name': None, 'type': None, 'lines': [],
118+
'source': None, 'comment': None})
119+
item_prefix = re.match(r'^\s*-\s*', line).group(0)
120+
groups[-1]['source'] = source_m.group(1).strip()
121+
continue
122+
if item_prefix is not None and line.strip():
123+
# Move the '- ' to the first line which is kept
124+
line = item_prefix + line.lstrip()
125+
item_prefix = None
126+
95127
contents.append(line)
96128

97129
if not line.strip():
@@ -108,41 +140,65 @@ def main():
108140
elif state == BLOCK_PAYLOADS:
109141
# Normalise list item: '- name: x' -> ' name: x'
110142
payload_line = re.sub(r'^\s*-\s*', ' ', line)
111-
name_m = re.match(r'^\s+name:\s*(.+)$', payload_line)
112-
type_m = re.match(r'^\s+type:\s*(.+)$', payload_line)
113-
if name_m:
114-
name = name_m.group(1).strip()
115-
target = (comment if comment is not None else name).strip()
116-
current_payload = {'name': name, 'target': target}
117-
src = os.path.join(cwd, target)
118-
dst = os.path.join(tmpdir, name)
119-
if not os.path.isfile(src) and not os.path.islink(src) and not os.path.isdir(src):
120-
if not os.path.exists(src):
121-
print(f'* error: file {target} not found')
122-
errors += 1
143+
if re.match(r'^\s*-', line) and not (groups and groups[-1]['lines'] == []):
144+
groups.append({'name': None, 'type': None, 'lines': [],
145+
'source': None, 'comment': None})
146+
if groups:
147+
groups[-1]['lines'].append(line)
148+
type_m = re.match(r'^\s+type:\s*(.+)$', payload_line)
149+
if type_m:
150+
groups[-1]['type'] = type_m.group(1).strip()
151+
name_m = re.match(r'^\s+name:\s*(.+)$', payload_line)
152+
if name_m:
153+
groups[-1]['name'] = name_m.group(1).strip()
154+
groups[-1]['comment'] = comment
155+
156+
for g in groups:
157+
name = g['name']
158+
if name is None:
159+
print("* error: payload has no name")
160+
errors += 1
161+
continue
162+
if g['source'] is not None:
163+
target = g['source']
164+
elif g['comment'] is not None:
165+
target = g['comment']
166+
else:
167+
target = name
168+
if not target:
169+
print(f"* error: payload {name}: empty source")
170+
errors += 1
171+
continue
172+
ptype = g['type']
173+
if ptype is None:
174+
print(f"* error: payload '{name}' has no type")
175+
errors += 1
176+
continue
177+
if ptype not in KNOWN_PAYLOAD_TYPES:
178+
print(f"* error: payload '{name}' has unknown type '{ptype}' "
179+
f"(expected one of: {', '.join(KNOWN_PAYLOAD_TYPES)})")
180+
errors += 1
181+
if ptype != "tmpfile" and target.endswith(COMPRESSED_SUFFIXES):
182+
print(f"* warning: payload '{name}' file '{target}' looks pre-compressed; "
183+
f"the artefact is already compressed as a whole, "
184+
f"did you mean '{re.sub(r'(?:'+'|'.join(re.escape(s) for s in COMPRESSED_SUFFIXES)+r')$', '', target)}'?")
185+
src = os.path.join(basedir, target)
186+
dst = os.path.join(tmpdir, name)
187+
if not os.path.isfile(src) and not os.path.islink(src) and not os.path.isdir(src):
188+
if not os.path.exists(src):
189+
print(f"* error: payload {name}: file '{target}' not found in {basedir}")
190+
errors += 1
191+
else:
192+
try:
193+
os.symlink(src, dst)
194+
payloads.append(name)
195+
except OSError:
196+
if os.path.isfile(src):
197+
shutil.copy2(src, dst)
198+
payloads.append(name)
123199
else:
124-
try:
125-
os.symlink(src, dst)
126-
payloads.append(name)
127-
except OSError:
128-
if os.path.isfile(src):
129-
shutil.copy2(src, dst)
130-
payloads.append(name)
131-
else:
132-
print(f'* error: cannot symlink {target}')
133-
errors += 1
134-
elif type_m:
135-
ptype = type_m.group(1).strip()
136-
pname = current_payload.get('name', '?')
137-
target = current_payload.get('target', pname)
138-
if ptype not in KNOWN_PAYLOAD_TYPES:
139-
print(f"* error: payload '{pname}' has unknown type '{ptype}' "
140-
f"(expected one of: {', '.join(KNOWN_PAYLOAD_TYPES)})")
200+
print(f"* error: payload {name}: cannot symlink '{target}'")
141201
errors += 1
142-
if ptype != "tmpfile" and target.endswith(COMPRESSED_SUFFIXES):
143-
print(f"* warning: payload '{pname}' file '{target}' looks pre-compressed; "
144-
f"the artefact is already compressed as a whole, "
145-
f"did you mean '{re.sub(r'(?:'+'|'.join(re.escape(s) for s in COMPRESSED_SUFFIXES)+r')$', '', target)}'?")
146202

147203
if errors:
148204
sys.exit(1)

0 commit comments

Comments
 (0)