Skip to content

Commit 853de76

Browse files
committed
bundle
1 parent 3277746 commit 853de76

File tree

10 files changed

+222
-7
lines changed

10 files changed

+222
-7
lines changed

Diff for: contrib/bash-completion/bob

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ __bob_clean()
121121

122122
__bob_cook()
123123
{
124-
if [[ "$prev" = "--destination" ]] ; then
124+
if [[ "$prev" = "--destination" || "$prev" == "--bundle" || "$prev" == "--unbundle" ]] ; then
125125
__bob_complete_dir "$cur"
126126
elif [[ "$prev" = "--download" ]] ; then
127127
__bob_complete_words "yes no deps forced forced-deps forced-fallback"
@@ -130,7 +130,7 @@ __bob_cook()
130130
elif [[ "$prev" = "--always-checkout" ]] ; then
131131
COMPREPLY=( )
132132
else
133-
__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"
133+
__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"
134134
fi
135135
}
136136

Diff for: doc/manpages/bob-build-dev.rst

+10
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``)

Diff for: doc/manpages/bob-build.rst

+1
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

Diff for: doc/manpages/bob-dev.rst

+1
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

Diff for: pym/bob/builder.py

+17-1
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

Diff for: pym/bob/bundle.py

+126
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+

Diff for: pym/bob/cmds/build/build.py

+13
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ def _downloadLayerArgument(arg):
214214
help="Move scm to attic if inline switch is not possible (default).")
215215
group.add_argument('--no-attic', action='store_false', default=None, dest='attic',
216216
help="Do not move to attic, instead fail the build.")
217+
parser.add_argument('--bundle', metavar='BUNDLE', default=None,
218+
help="Bundle all matching packages to BUNDLE")
219+
parser.add_argument('--bundle-exclude', action='append', default=[],
220+
help="Do not add matching packages to bundle.")
221+
parser.add_argument('--unbundle', default=[], action='append',
222+
help="Use sources from bundle")
217223
args = parser.parse_args(argv)
218224

219225
defines = processDefines(args.defines)
@@ -230,6 +236,7 @@ def _downloadLayerArgument(arg):
230236
if args.build_mode != 'build-only':
231237
setVerbosity(args.verbose)
232238
updateLayers(recipes, loop, defines, args.verbose, args.attic, args.layerConfig)
239+
recipes.setBundleFiles(args.unbundle)
233240
recipes.parse(defines)
234241

235242
# if arguments are not passed on cmdline use them from default.yaml or set to default yalue
@@ -302,6 +309,9 @@ def _downloadLayerArgument(arg):
302309
packages = recipes.generatePackages(nameFormatter, args.sandbox)
303310
if develop: developPersister.prime(packages)
304311

312+
if args.bundle and args.build_mode == 'build-only':
313+
parser.error("--bundle can't be used with --build-only")
314+
305315
verbosity = cfg.get('verbosity', 0) + args.verbose - args.quiet
306316
setVerbosity(verbosity)
307317
builder = LocalBuilder(verbosity, args.force,
@@ -325,6 +335,7 @@ def _downloadLayerArgument(arg):
325335
builder.setShareHandler(getShare(recipes.getShareConfig()))
326336
builder.setShareMode(args.shared, args.install)
327337
builder.setAtticEnable(args.attic)
338+
builder.setBundle(args.bundle, args.bundle_exclude)
328339
if args.resume: builder.loadBuildState()
329340

330341
backlog = []
@@ -386,6 +397,8 @@ def _downloadLayerArgument(arg):
386397
+ " package" + ("s" if (stats.packagesBuilt != 1) else "") + " built, "
387398
+ str(stats.packagesDownloaded) + " downloaded.")
388399

400+
builder.bundle()
401+
389402
# Copy build result if requested. It's ok to overwrite files that are
390403
# already at the destination. Warn if built packages overwrite
391404
# themselves, though.

0 commit comments

Comments
 (0)