Skip to content

Commit 5ae94e9

Browse files
committed
bundle
1 parent 080fc60 commit 5ae94e9

File tree

10 files changed

+222
-7
lines changed

10 files changed

+222
-7
lines changed

contrib/bash-completion/bob

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ __bob_clean()
118118

119119
__bob_cook()
120120
{
121-
if [[ "$prev" = "--destination" ]] ; then
121+
if [[ "$prev" = "--destination" || "$prev" == "--bundle" || "$prev" == "--unbundle" ]] ; then
122122
__bob_complete_dir "$cur"
123123
elif [[ "$prev" = "--download" ]] ; then
124124
__bob_complete_words "yes no deps forced forced-deps forced-fallback"
@@ -127,7 +127,7 @@ __bob_cook()
127127
elif [[ "$prev" = "--always-checkout" ]] ; then
128128
COMPREPLY=( )
129129
else
130-
__bob_complete_path "--destination -j --jobs -k --keep-going -f --force -n --no-deps -p --with-provided --without-provided -A --no-audit --audit -b --build-only -B --checkout-only --normal --clean --incremental --always-checkout --resume -q --quiet -v --verbose --no-logfiles -D -c -e -E -M --upload --link-deps --no-link-deps --download --download-layer --shared --no-shared --install --no-install --sandbox --no-sandbox --clean-checkout --attic --no-attic"
130+
__bob_complete_path "--destination -j --jobs -k --keep-going -f --force -n --no-deps -p --with-provided --without-provided -A --no-audit --audit -b --build-only -B --checkout-only --normal --clean --incremental --always-checkout --resume -q --quiet -v --verbose --no-logfiles -D -c -e -E -M --upload --link-deps --no-link-deps --download --download-layer --shared --no-shared --install --no-install --sandbox --no-sandbox --clean-checkout --attic --no-attic --bundle --bundle-exclude --unbundle"
131131
fi
132132
}
133133

doc/manpages/bob-build-dev.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ Options
3535

3636
This is the default unless the user changed it in ``default.yaml``.
3737

38+
``--bundle BUNDLE``
39+
Bundle all the sources needed to build the package. The bunlde is a tar-file
40+
containing the sources and a overrides file. To use the bundle call bob
41+
dev/build with ``-c`` pointing to the scmOverrides-file. In addition to this
42+
the ``LOCAL_BUNDLE_BASE`` environment variable needs to be set to point to
43+
the base-directoy where the bundle has been extracted.
44+
45+
``--bundle-exclude RE``
46+
Do not add packages matching RE to the bundle.
47+
3848
``--clean``
3949
Do clean builds by clearing the build directory before executing the build
4050
commands. It will *not* clean all build results (e.g. like ``make clean``)

doc/manpages/bob-build.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Synopsis
2424
[--shared | --no-shared] [--install | --no-install]
2525
[--sandbox | --no-sandbox] [--clean-checkout]
2626
[--attic | --no-attic]
27+
[--bundle BUNDLE] [--bundle-exclude BUNDLE_EXCLUDE]
2728
PACKAGE [PACKAGE ...]
2829

2930

doc/manpages/bob-dev.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Synopsis
2424
[--shared | --no-shared] [--install | --no-install]
2525
[--sandbox | --no-sandbox] [--clean-checkout]
2626
[--attic | --no-attic]
27+
[--bundle BUNDLE] [--bundle-exclude BUNDLE_EXCLUDE]
2728
PACKAGE [PACKAGE ...]
2829

2930

pym/bob/builder.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from . import BOB_VERSION
77
from .archive import DummyArchive
88
from .audit import Audit
9+
from .bundle import Bundler
910
from .errors import BobError, BuildError, MultiBobError
1011
from .input import RecipeSet
1112
from .invoker import Invoker, InvocationMode
@@ -407,6 +408,7 @@ def __init__(self, verbose, force, skipDeps, buildOnly, preserveEnv,
407408
self.__installSharedPackages = False
408409
self.__executor = None
409410
self.__attic = True
411+
self.__bundler = None
410412

411413
def setExecutor(self, executor):
412414
self.__executor = executor
@@ -505,6 +507,10 @@ def setAuditMeta(self, keys):
505507
def setAtticEnable(self, enable):
506508
self.__attic = enable
507509

510+
def setBundle(self, dest, excludes):
511+
if dest is not None:
512+
self.__bundler = Bundler(dest, excludes)
513+
508514
def setShareHandler(self, handler):
509515
self.__share = handler
510516

@@ -618,6 +624,10 @@ def __workspaceLock(self, step):
618624
self.__workspaceLocks[path] = ret = asyncio.Lock()
619625
return ret
620626

627+
def bundle(self):
628+
if self.__bundler:
629+
self.__bundler.finalize()
630+
621631
async def _generateAudit(self, step, depth, resultHash, buildId, executed=True):
622632
auditPath = os.path.join(os.path.dirname(step.getWorkspacePath()), "audit.json.gz")
623633
if os.path.lexists(auditPath): removePath(auditPath)
@@ -1237,7 +1247,10 @@ async def _cookCheckoutStep(self, checkoutStep, depth):
12371247
oldCheckoutHash = datetime.datetime.now()
12381248
BobState().setResultHash(prettySrcPath, oldCheckoutHash)
12391249

1240-
with stepExec(checkoutStep, "CHECKOUT",
1250+
action = "CHECKOUT"
1251+
if checkoutStep.getBundle() is not None:
1252+
action = "UNBUNDLE"
1253+
with stepExec(checkoutStep, action,
12411254
"{} ({}) {}".format(prettySrcPath, checkoutReason, overridesString)) as a:
12421255
await self._runShell(checkoutStep, "checkout", a)
12431256
self.__statistic.checkouts += 1
@@ -1284,6 +1297,9 @@ async def _cookCheckoutStep(self, checkoutStep, depth):
12841297
assert predicted, "Non-predicted incorrect Build-Id found!"
12851298
self.__handleChangedBuildId(checkoutStep, checkoutHash)
12861299

1300+
if self.__bundler:
1301+
await self.__bundler.bundle(checkoutStep, self.__executor)
1302+
12871303
async def _cookBuildStep(self, buildStep, depth, buildBuildId):
12881304
# Add the execution path of the build step to the buildDigest to
12891305
# detect changes between sandbox and non-sandbox builds. This is

pym/bob/bundle.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Bob build tool
2+
# Copyright (C) 2024 Secunet Security Networks AG
3+
#
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
from .errors import BuildError
7+
from .tty import stepExec, EXECUTED
8+
from .utils import hashFile
9+
10+
import asyncio
11+
import concurrent.futures
12+
import fnmatch
13+
import gzip
14+
import hashlib
15+
import os
16+
import schema
17+
import signal
18+
import tarfile
19+
import tempfile
20+
import yaml
21+
22+
class Bundler:
23+
def __init__(self, name, excludes):
24+
self.__name = name
25+
self.__bundleFile = os.path.join(os.getcwd(), self.__name) + ".tar"
26+
self.__excludes = excludes
27+
self.__tempDir = tempfile.TemporaryDirectory()
28+
self.__tempDirPath = os.path.join(self.__tempDir.name, self.__name)
29+
self.__bundled = {}
30+
31+
if os.path.exists(self.__bundleFile):
32+
raise BuildError(f"Bundle {self.__bundleFile} already exists!")
33+
os.mkdir(self.__tempDirPath)
34+
35+
def _bundle(self, workspace, bundleFile):
36+
def reset(tarinfo):
37+
tarinfo.uid = tarinfo.gid = 0
38+
tarinfo.uname = tarinfo.gname = "root"
39+
tarinfo.mtime = 0
40+
return tarinfo
41+
42+
# Set default signal handler so that KeyboardInterrupt is raised.
43+
# Needed to gracefully handle ctrl+c.
44+
signal.signal(signal.SIGINT, signal.default_int_handler)
45+
46+
try:
47+
files = []
48+
for root, dirs, filenames in os.walk(workspace):
49+
for f in filenames:
50+
files.append(os.path.join(root, f))
51+
files.sort()
52+
with open(bundleFile, 'wb') as outfile:
53+
with gzip.GzipFile(fileobj=outfile, mode='wb', mtime=0) as zipfile:
54+
with tarfile.open(fileobj=zipfile, mode="w:") as bundle:
55+
for f in files:
56+
bundle.add(f, arcname=os.path.relpath(f, workspace),
57+
recursive=False, filter=reset)
58+
digest = hashFile(bundleFile, hashlib.sha256).hex()
59+
60+
except (tarfile.TarError, OSError) as e:
61+
raise BuildError("Cannot bundle workspace: " + str(e))
62+
finally:
63+
# Restore signals to default so that Ctrl+C kills process. Needed
64+
# to prevent ugly backtraces when user presses ctrl+c.
65+
signal.signal(signal.SIGINT, signal.SIG_DFL)
66+
67+
return ("ok", EXECUTED, digest)
68+
69+
async def bundle(self, step, executor):
70+
for e in self.__excludes:
71+
if fnmatch.fnmatch(step.getPackage().getName(), e): return
72+
73+
checkoutVariantId = step.getPackage().getCheckoutStep().getVariantId().hex()
74+
dest = os.path.join(self.__tempDirPath, step.getPackage().getRecipe().getName(),
75+
checkoutVariantId)
76+
os.makedirs(dest)
77+
bundleFile = os.path.join(dest, "bundle.tgz")
78+
79+
loop = asyncio.get_event_loop()
80+
with stepExec(step, "BUNDLE", "{}".format(step.getWorkspacePath())) as a:
81+
try:
82+
msg, kind, digest = await loop.run_in_executor(executor, Bundler._bundle,
83+
self, step.getWorkspacePath(), bundleFile)
84+
a.setResult(msg, kind)
85+
except (concurrent.futures.CancelledError, concurrent.futures.process.BrokenProcessPool):
86+
raise BuildError("Upload of bundling interrupted.")
87+
88+
self.__bundled[checkoutVariantId] = (step.getPackage().getRecipe().getName(), digest, bundleFile)
89+
90+
def finalize(self):
91+
bundle = []
92+
with tarfile.open(self.__bundleFile, "w") as bundle_tar:
93+
94+
for vid, (package, digest, bundleFile) in sorted(self.__bundled.items()):
95+
bundle.append({vid : {"digestSHA256" : digest,
96+
"name" : package}})
97+
print(f"add to bundle: {bundleFile}")
98+
bundle_tar.add(bundleFile,
99+
arcname=os.path.relpath(bundleFile, self.__tempDir.name))
100+
101+
bundleConfig = self.__name + ".yaml"
102+
bundleConfigPath = os.path.join(self.__tempDirPath, bundleConfig)
103+
with open(bundleConfigPath, "w") as f:
104+
yaml.dump(bundle, f, default_flow_style=False)
105+
bundle_tar.add(bundleConfigPath, arcname=os.path.join(self.__name, bundleConfig))
106+
107+
class Unbundler:
108+
BUNDLE_SCHEMA = schema.Schema([{
109+
str : schema.Schema({
110+
"name" : str,
111+
"digestSHA256" : str
112+
})
113+
}])
114+
115+
def __init__(self, bundles):
116+
self.__bundles = bundles
117+
118+
def getFromBundle(self, variantId):
119+
for bundleFile, items in self.__bundles.items():
120+
for b in items:
121+
if variantId.hex() in b:
122+
data = b.get(variantId.hex())
123+
return (bundleFile, os.path.join(os.path.dirname(bundleFile), data['name'], variantId.hex(),
124+
"bundle.tgz"), data['digestSHA256'])
125+
return None
126+

pym/bob/cmds/build/build.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ def _downloadLayerArgument(arg):
211211
help="Move scm to attic if inline switch is not possible (default).")
212212
group.add_argument('--no-attic', action='store_false', default=None, dest='attic',
213213
help="Do not move to attic, instead fail the build.")
214+
parser.add_argument('--bundle', metavar='BUNDLE', default=None,
215+
help="Bundle all matching packages to BUNDLE")
216+
parser.add_argument('--bundle-exclude', action='append', default=[],
217+
help="Do not add matching packages to bundle.")
218+
parser.add_argument('--unbundle', default=[], action='append',
219+
help="Use sources from bundle")
214220
args = parser.parse_args(argv)
215221

216222
defines = processDefines(args.defines)
@@ -224,6 +230,7 @@ def _downloadLayerArgument(arg):
224230
recipes.defineHook('developNameFormatter', LocalBuilder.developNameFormatter)
225231
recipes.defineHook('developNamePersister', None)
226232
recipes.setConfigFiles(args.configFile)
233+
recipes.setBundleFiles(args.unbundle)
227234
recipes.parse(defines)
228235

229236
# if arguments are not passed on cmdline use them from default.yaml or set to default yalue
@@ -296,6 +303,9 @@ def _downloadLayerArgument(arg):
296303
packages = recipes.generatePackages(nameFormatter, args.sandbox)
297304
if develop: developPersister.prime(packages)
298305

306+
if args.bundle and args.build_mode == 'build-only':
307+
parser.error("--bundle can't be used with --build-only")
308+
299309
verbosity = cfg.get('verbosity', 0) + args.verbose - args.quiet
300310
setVerbosity(verbosity)
301311
builder = LocalBuilder(verbosity, args.force,
@@ -319,6 +329,7 @@ def _downloadLayerArgument(arg):
319329
builder.setShareHandler(getShare(recipes.getShareConfig()))
320330
builder.setShareMode(args.shared, args.install)
321331
builder.setAtticEnable(args.attic)
332+
builder.setBundle(args.bundle, args.bundle_exclude)
322333
if args.resume: builder.loadBuildState()
323334

324335
backlog = []
@@ -380,6 +391,8 @@ def _downloadLayerArgument(arg):
380391
+ " package" + ("s" if (stats.packagesBuilt != 1) else "") + " built, "
381392
+ str(stats.packagesDownloaded) + " downloaded.")
382393

394+
builder.bundle()
395+
383396
# Copy build result if requested. It's ok to overwrite files that are
384397
# already at the destination. Warn if built packages overwrite
385398
# themselves, though.

0 commit comments

Comments
 (0)