Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: bundle #586

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contrib/bash-completion/bob
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ __bob_clean()

__bob_cook()
{
if [[ "$prev" = "--destination" ]] ; then
if [[ "$prev" = "--destination" || "$prev" == "--bundle" || "$prev" == "--unbundle" ]] ; then
__bob_complete_dir "$cur"
elif [[ "$prev" = "--download" ]] ; then
__bob_complete_words "yes no deps forced forced-deps forced-fallback"
Expand All @@ -130,7 +130,7 @@ __bob_cook()
elif [[ "$prev" = "--always-checkout" ]] ; then
COMPREPLY=( )
else
__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"
__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"
fi
}

Expand Down
10 changes: 10 additions & 0 deletions doc/manpages/bob-build-dev.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ Options

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

``--bundle BUNDLE``
Bundle all the sources needed to build the package. The bunlde is a tar-file
containing the sources and a overrides file. To use the bundle call bob
dev/build with ``-c`` pointing to the scmOverrides-file. In addition to this
the ``LOCAL_BUNDLE_BASE`` environment variable needs to be set to point to
the base-directoy where the bundle has been extracted.

``--bundle-exclude RE``
Do not add packages matching RE to the bundle.

``--clean``
Do clean builds by clearing the build directory before executing the build
commands. It will *not* clean all build results (e.g. like ``make clean``)
Expand Down
1 change: 1 addition & 0 deletions doc/manpages/bob-build.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Synopsis
[--shared | --no-shared] [--install | --no-install]
[--sandbox | --no-sandbox] [--clean-checkout]
[--attic | --no-attic]
[--bundle BUNDLE] [--bundle-exclude BUNDLE_EXCLUDE]
PACKAGE [PACKAGE ...]


Expand Down
1 change: 1 addition & 0 deletions doc/manpages/bob-dev.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Synopsis
[--shared | --no-shared] [--install | --no-install]
[--sandbox | --no-sandbox] [--clean-checkout]
[--attic | --no-attic]
[--bundle BUNDLE] [--bundle-exclude BUNDLE_EXCLUDE]
PACKAGE [PACKAGE ...]


Expand Down
18 changes: 17 additions & 1 deletion pym/bob/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from . import BOB_VERSION
from .archive import DummyArchive
from .audit import Audit
from .bundle import Bundler
from .errors import BobError, BuildError, MultiBobError
from .input import RecipeSet
from .invoker import Invoker, InvocationMode
Expand Down Expand Up @@ -407,6 +408,7 @@
self.__installSharedPackages = False
self.__executor = None
self.__attic = True
self.__bundler = None

def setExecutor(self, executor):
self.__executor = executor
Expand Down Expand Up @@ -505,6 +507,10 @@
def setAtticEnable(self, enable):
self.__attic = enable

def setBundle(self, dest, excludes):
if dest is not None:
self.__bundler = Bundler(dest, excludes)

Check warning on line 512 in pym/bob/builder.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/builder.py#L512

Added line #L512 was not covered by tests

def setShareHandler(self, handler):
self.__share = handler

Expand Down Expand Up @@ -618,6 +624,10 @@
self.__workspaceLocks[path] = ret = asyncio.Lock()
return ret

def bundle(self):
if self.__bundler:
self.__bundler.finalize()

Check warning on line 629 in pym/bob/builder.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/builder.py#L629

Added line #L629 was not covered by tests

async def _generateAudit(self, step, depth, resultHash, buildId, executed=True):
auditPath = os.path.join(os.path.dirname(step.getWorkspacePath()), "audit.json.gz")
if os.path.lexists(auditPath): removePath(auditPath)
Expand Down Expand Up @@ -1237,7 +1247,10 @@
oldCheckoutHash = datetime.datetime.now()
BobState().setResultHash(prettySrcPath, oldCheckoutHash)

with stepExec(checkoutStep, "CHECKOUT",
action = "CHECKOUT"
if checkoutStep.getBundle() is not None:
action = "UNBUNDLE"

Check warning on line 1252 in pym/bob/builder.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/builder.py#L1252

Added line #L1252 was not covered by tests
with stepExec(checkoutStep, action,
"{} ({}) {}".format(prettySrcPath, checkoutReason, overridesString)) as a:
await self._runShell(checkoutStep, "checkout", a)
self.__statistic.checkouts += 1
Expand Down Expand Up @@ -1284,6 +1297,9 @@
assert predicted, "Non-predicted incorrect Build-Id found!"
self.__handleChangedBuildId(checkoutStep, checkoutHash)

if self.__bundler:
await self.__bundler.bundle(checkoutStep, self.__executor)

Check warning on line 1301 in pym/bob/builder.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/builder.py#L1301

Added line #L1301 was not covered by tests

async def _cookBuildStep(self, buildStep, depth, buildBuildId):
# Add the execution path of the build step to the buildDigest to
# detect changes between sandbox and non-sandbox builds. This is
Expand Down
126 changes: 126 additions & 0 deletions pym/bob/bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Bob build tool
# Copyright (C) 2024 Secunet Security Networks AG
#
# SPDX-License-Identifier: GPL-3.0-or-later

from .errors import BuildError
from .tty import stepExec, EXECUTED
from .utils import hashFile

import asyncio
import concurrent.futures
import fnmatch
import gzip
import hashlib
import os
import schema
import signal
import tarfile
import tempfile
import yaml

class Bundler:
def __init__(self, name, excludes):
self.__name = name
self.__bundleFile = os.path.join(os.getcwd(), self.__name) + ".tar"
self.__excludes = excludes
self.__tempDir = tempfile.TemporaryDirectory()
self.__tempDirPath = os.path.join(self.__tempDir.name, self.__name)
self.__bundled = {}

Check warning on line 29 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L24-L29

Added lines #L24 - L29 were not covered by tests

if os.path.exists(self.__bundleFile):
raise BuildError(f"Bundle {self.__bundleFile} already exists!")
os.mkdir(self.__tempDirPath)

Check warning on line 33 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L31-L33

Added lines #L31 - L33 were not covered by tests

def _bundle(self, workspace, bundleFile):
def reset(tarinfo):
tarinfo.uid = tarinfo.gid = 0
tarinfo.uname = tarinfo.gname = "root"
tarinfo.mtime = 0
return tarinfo

Check warning on line 40 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L36-L40

Added lines #L36 - L40 were not covered by tests

# Set default signal handler so that KeyboardInterrupt is raised.
# Needed to gracefully handle ctrl+c.
signal.signal(signal.SIGINT, signal.default_int_handler)

Check warning on line 44 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L44

Added line #L44 was not covered by tests

try:
files = []
for root, dirs, filenames in os.walk(workspace):
for f in filenames:
files.append(os.path.join(root, f))
files.sort()
with open(bundleFile, 'wb') as outfile:
with gzip.GzipFile(fileobj=outfile, mode='wb', mtime=0) as zipfile:
with tarfile.open(fileobj=zipfile, mode="w:") as bundle:
for f in files:
bundle.add(f, arcname=os.path.relpath(f, workspace),

Check warning on line 56 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L46-L56

Added lines #L46 - L56 were not covered by tests
recursive=False, filter=reset)
digest = hashFile(bundleFile, hashlib.sha256).hex()

Check warning on line 58 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L58

Added line #L58 was not covered by tests

except (tarfile.TarError, OSError) as e:
raise BuildError("Cannot bundle workspace: " + str(e))

Check warning on line 61 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L60-L61

Added lines #L60 - L61 were not covered by tests
finally:
# Restore signals to default so that Ctrl+C kills process. Needed
# to prevent ugly backtraces when user presses ctrl+c.
signal.signal(signal.SIGINT, signal.SIG_DFL)

Check warning on line 65 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L65

Added line #L65 was not covered by tests

return ("ok", EXECUTED, digest)

Check warning on line 67 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L67

Added line #L67 was not covered by tests

async def bundle(self, step, executor):
for e in self.__excludes:
if fnmatch.fnmatch(step.getPackage().getName(), e): return

Check warning on line 71 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L70-L71

Added lines #L70 - L71 were not covered by tests

checkoutVariantId = step.getPackage().getCheckoutStep().getVariantId().hex()
dest = os.path.join(self.__tempDirPath, step.getPackage().getRecipe().getName(),

Check warning on line 74 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L73-L74

Added lines #L73 - L74 were not covered by tests
checkoutVariantId)
os.makedirs(dest)
bundleFile = os.path.join(dest, "bundle.tgz")

Check warning on line 77 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L76-L77

Added lines #L76 - L77 were not covered by tests

loop = asyncio.get_event_loop()
with stepExec(step, "BUNDLE", "{}".format(step.getWorkspacePath())) as a:
try:
msg, kind, digest = await loop.run_in_executor(executor, Bundler._bundle,

Check warning on line 82 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L79-L82

Added lines #L79 - L82 were not covered by tests
self, step.getWorkspacePath(), bundleFile)
a.setResult(msg, kind)
except (concurrent.futures.CancelledError, concurrent.futures.process.BrokenProcessPool):
raise BuildError("Upload of bundling interrupted.")

Check warning on line 86 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L84-L86

Added lines #L84 - L86 were not covered by tests

self.__bundled[checkoutVariantId] = (step.getPackage().getRecipe().getName(), digest, bundleFile)

Check warning on line 88 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L88

Added line #L88 was not covered by tests

def finalize(self):
bundle = []
with tarfile.open(self.__bundleFile, "w") as bundle_tar:

Check warning on line 92 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L91-L92

Added lines #L91 - L92 were not covered by tests

for vid, (package, digest, bundleFile) in sorted(self.__bundled.items()):
bundle.append({vid : {"digestSHA256" : digest,

Check warning on line 95 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L94-L95

Added lines #L94 - L95 were not covered by tests
"name" : package}})
print(f"add to bundle: {bundleFile}")
bundle_tar.add(bundleFile,

Check warning on line 98 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L97-L98

Added lines #L97 - L98 were not covered by tests
arcname=os.path.relpath(bundleFile, self.__tempDir.name))

bundleConfig = self.__name + ".yaml"
bundleConfigPath = os.path.join(self.__tempDirPath, bundleConfig)
with open(bundleConfigPath, "w") as f:
yaml.dump(bundle, f, default_flow_style=False)
bundle_tar.add(bundleConfigPath, arcname=os.path.join(self.__name, bundleConfig))

Check warning on line 105 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L101-L105

Added lines #L101 - L105 were not covered by tests

class Unbundler:
BUNDLE_SCHEMA = schema.Schema([{
str : schema.Schema({
"name" : str,
"digestSHA256" : str
})
}])

def __init__(self, bundles):
self.__bundles = bundles

Check warning on line 116 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L116

Added line #L116 was not covered by tests

def getFromBundle(self, variantId):
for bundleFile, items in self.__bundles.items():
for b in items:
if variantId.hex() in b:
data = b.get(variantId.hex())
return (bundleFile, os.path.join(os.path.dirname(bundleFile), data['name'], variantId.hex(),

Check warning on line 123 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L119-L123

Added lines #L119 - L123 were not covered by tests
"bundle.tgz"), data['digestSHA256'])
return None

Check warning on line 125 in pym/bob/bundle.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/bundle.py#L125

Added line #L125 was not covered by tests

13 changes: 13 additions & 0 deletions pym/bob/cmds/build/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,12 @@
help="Move scm to attic if inline switch is not possible (default).")
group.add_argument('--no-attic', action='store_false', default=None, dest='attic',
help="Do not move to attic, instead fail the build.")
parser.add_argument('--bundle', metavar='BUNDLE', default=None,
help="Bundle all matching packages to BUNDLE")
parser.add_argument('--bundle-exclude', action='append', default=[],
help="Do not add matching packages to bundle.")
parser.add_argument('--unbundle', default=[], action='append',
help="Use sources from bundle")
args = parser.parse_args(argv)

defines = processDefines(args.defines)
Expand All @@ -230,6 +236,7 @@
if args.build_mode != 'build-only':
setVerbosity(args.verbose)
updateLayers(recipes, loop, defines, args.verbose, args.attic, args.layerConfig)
recipes.setBundleFiles(args.unbundle)
recipes.parse(defines)

# if arguments are not passed on cmdline use them from default.yaml or set to default yalue
Expand Down Expand Up @@ -302,6 +309,9 @@
packages = recipes.generatePackages(nameFormatter, args.sandbox)
if develop: developPersister.prime(packages)

if args.bundle and args.build_mode == 'build-only':
parser.error("--bundle can't be used with --build-only")

Check warning on line 313 in pym/bob/cmds/build/build.py

View check run for this annotation

Codecov / codecov/patch

pym/bob/cmds/build/build.py#L313

Added line #L313 was not covered by tests

verbosity = cfg.get('verbosity', 0) + args.verbose - args.quiet
setVerbosity(verbosity)
builder = LocalBuilder(verbosity, args.force,
Expand All @@ -325,6 +335,7 @@
builder.setShareHandler(getShare(recipes.getShareConfig()))
builder.setShareMode(args.shared, args.install)
builder.setAtticEnable(args.attic)
builder.setBundle(args.bundle, args.bundle_exclude)
if args.resume: builder.loadBuildState()

backlog = []
Expand Down Expand Up @@ -386,6 +397,8 @@
+ " package" + ("s" if (stats.packagesBuilt != 1) else "") + " built, "
+ str(stats.packagesDownloaded) + " downloaded.")

builder.bundle()

# Copy build result if requested. It's ok to overwrite files that are
# already at the destination. Warn if built packages overwrite
# themselves, though.
Expand Down
Loading