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

Container commands #60

Open
wants to merge 2 commits 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
1 change: 1 addition & 0 deletions ci/bin/exclude-pyrex-capture-state
23 changes: 23 additions & 0 deletions ci/bin/pyrex-capture-state
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#! /bin/sh
#
# Copyright 2019-2020 Garmin Ltd. or its subsidiaries
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -eu

if [ "$1" = "podman" ]; then
cp --no-preserve=all /proc/1/cmdline $2
else
cat /proc/self/cgroup > $2
fi
123 changes: 92 additions & 31 deletions ci/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,20 @@ class PyrexImageType_base(PyrexTest):
from this class
"""

def capture_local_state(self):
if self.provider == "podman":
with open("/proc/1/cmdline", "rb") as f:
return f.read().decode("utf-8")

with open("/proc/self/cgroup", "rb") as f:
return f.read().decode("utf-8")

def assertStateInContainer(self, pyrex_state):
self.assertNotEqual(self.capture_local_state(), pyrex_state)

def assertStateNotInContainer(self, pyrex_state):
self.assertEqual(self.capture_local_state(), pyrex_state)

def test_init(self):
self.assertPyrexHostCommand("true")

Expand All @@ -313,34 +327,20 @@ def test_pyrex_run(self):
def test_in_container(self):
def capture_pyrex_state(*args, **kwargs):
capture_file = os.path.join(self.thread_dir, "pyrex_capture")
self.assertPyrexContainerCommand(
"%s %s %s"
% (
os.path.join(PYREX_ROOT, "ci", "bin", "pyrex-capture-state"),
self.provider,
capture_file,
),
*args,
**kwargs
)
with open(capture_file, "rb") as f:
return f.read().decode("utf-8")

if self.provider == "podman":
self.assertPyrexContainerShellCommand(
"cp --no-preserve=all /proc/1/cmdline %s" % capture_file,
*args,
**kwargs
)
with open(capture_file, "rb") as f:
return f.read()
else:
self.assertPyrexContainerShellCommand(
"cat /proc/self/cgroup > %s" % capture_file, *args, **kwargs
)
with open(capture_file, "r") as f:
return f.read()

def capture_local_state():
if self.provider == "podman":
with open("/proc/1/cmdline", "rb") as f:
return f.read()
else:
with open("/proc/self/cgroup", "r") as f:
return f.read()

local_state = capture_local_state()

pyrex_state = capture_pyrex_state()
self.assertNotEqual(local_state, pyrex_state)
self.assertStateInContainer(capture_pyrex_state())

def test_quiet_build(self):
env = os.environ.copy()
Expand Down Expand Up @@ -628,6 +628,44 @@ def test_required_terms(self):
)
self.assertEqual(output, t, msg="Bad $TERM found in container!")

def test_term_enabled(self):
env = os.environ.copy()
env["TERM"] = "dumb"
output = self.assertPyrexContainerShellPTY(
"test -t 1; echo $?",
env=env,
quiet_init=True,
)
self.assertEqual(output, "0", msg="Stdout is not a TTY")

del env["TERM"]
output = self.assertPyrexContainerShellPTY(
"test -t 1; echo $?",
env=env,
quiet_init=True,
)
self.assertEqual(output, "0", msg="Stdout is not a TTY")

def test_term_disabled(self):
env = os.environ.copy()
env["TERM"] = "dumb"
output = self.assertPyrexContainerShellCommand(
"test -t 1; echo $?",
env=env,
quiet_init=True,
capture=True,
)
self.assertEqual(output, "1", msg="Stdout is a TTY when terminal is not a TTY")

del env["TERM"]
output = self.assertPyrexContainerShellCommand(
"test -t 1; echo $?",
env=env,
quiet_init=True,
capture=True,
)
self.assertEqual(output, "1", msg="Stdout is a TTY when terminal is not a TTY")

def test_tini(self):
self.assertPyrexContainerCommand("tini --version")

Expand Down Expand Up @@ -808,24 +846,47 @@ def test_pyrex_mkconfig(self):
self.assertSubprocess(cmd + [out_file], cwd=self.build_dir, returncode=1)

def test_user_commands(self):
def capture_pyrex_state(command, *args, **kwargs):
capture_file = os.path.join(self.thread_dir, "pyrex_capture")
self.assertPyrexHostCommand(
"%s %s %s" % (command, self.provider, capture_file), *args, **kwargs
)
with open(capture_file, "rb") as f:
return f.read().decode("utf-8")

conf = self.get_config()
conf["config"]["commands"] = "/bin/true !/bin/false"
conf["config"][
"commands"
] = """\
${env:PYREX_CONFIG_BIND}/ci/bin/*
!${env:PYREX_CONFIG_BIND}/ci/bin/exclude-pyrex-capture-state
/bin/true
!/bin/false
"""
conf["config"].setdefault("envimport", "")
conf["config"]["envimport"] += " PYREX_CONFIG_BIND"

conf.write_conf()

self.assertStateInContainer(capture_pyrex_state("pyrex-capture-state"))
self.assertStateNotInContainer(
capture_pyrex_state("exclude-pyrex-capture-state")
)

self.assertPyrexHostCommand("/bin/true")
self.assertPyrexHostCommand("/bin/false", returncode=1)

true_path = self.assertPyrexHostCommand(
"which true", capture=True, quiet_init=True
)
true_link_path = os.readlink(true_path)
self.assertEqual(os.path.basename(true_link_path), "exec-shim-pyrex")
self.assertFalse(os.path.islink(true_path))
self.assertNotEqual(true_path, "/bin/true")

false_path = self.assertPyrexHostCommand(
"which false", capture=True, quiet_init=True
)
false_link_path = os.readlink(false_path)
self.assertEqual(os.path.basename(false_link_path), "false")
self.assertEqual(false_link_path, "/bin/false")


class PyrexImageType_oe(PyrexImageType_base):
Expand Down
24 changes: 16 additions & 8 deletions pyrex.ini
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@
# to be specified in the user config file
confversion = @CONFVERSION@

# A list of globs for commands that should be wrapped by Pyrex. Overrides the
# command set specified by the container itself when it was captured. Any path
# starting with a "!" will be excluded from being wrapped by Pyrex and will
# run directly in the host environment
#commands =
# ${env:PYREX_OEROOT}/bitbake/bin/*
# ${env:PYREX_OEROOT}/scripts/*
# !${env:PYREX_OEROOT}/scripts/runqemu*
# A list of additional globs for commands that should be wrapped by Pyrex.
# Appends to the the command set specified by the container itself when it was
# captured. Any path starting with a "!" will be excluded from being wrapped by
# Pyrex and will run directly in the host environment. You will often want to
# root these with an environment variable, remembering to import it with envimport.
# $PYREX_CONFIG_BIND is usually a good choice here, but you can set and use
# your own variable if desired. As an example:
#
# ${env:PYREX_CONFIG_BIND}/bin/*
# !${env:PYREX_CONFIG_BIND}/bin/exclude_this
#
# Note that any command listed here will take precedence over a command with
# the same name specified by the container, and excludes take precedence over
# includes. Thus, you can override the containers decision to include or
# exclude a command by adding it here.
%commands =

# The Container engine executable (e.g. docker, podman) to use. If the path
# does not start with a "/", the $PATH environment variable will be searched
Expand Down
59 changes: 42 additions & 17 deletions pyrex.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,35 +665,60 @@ def create_shims(config, build_config, buildconf):
os.chmod(rebuildfile, stat.S_IRWXU)

# Create shims
user_commands = config["config"].get("commands")
if user_commands:
user_commands = user_commands.split()
command_globs = [c for c in user_commands if not c.startswith("!")]
nopyrex_globs = [c[1:] for c in user_commands if c.startswith("!")]
else:
command_globs = build_config["container"].get("commands", {}).get("include", {})
nopyrex_globs = build_config["container"].get("commands", {}).get("exclude", {})

commands = set()

def add_commands(globs, target):
def glob_commands(globs):
nonlocal commands

for g in globs:
for cmd in glob.iglob(g):
norm_cmd = os.path.normpath(cmd)
base = os.path.basename(cmd)
if (
norm_cmd not in commands
base not in commands
and os.path.isfile(cmd)
and os.access(cmd, os.X_OK)
):
commands.add(norm_cmd)
name = os.path.basename(cmd)
commands.add(base)
shim = os.path.join(shimdir, base)
yield cmd, shim

# User commands
user_commands = config["config"].get("commands", "").split()

user_excludes = [c[1:] for c in user_commands if c.startswith("!")]
for cmd, shim in glob_commands(user_excludes):
os.symlink(cmd, shim)

user_includes = [c for c in user_commands if not c.startswith("!")]
for cmd, shim in glob_commands(user_includes):
# User commands need dedicates shims that execute with the full path,
# since we don't know if they are in $PATH
with open(shim, "w") as f:
f.write(
textwrap.dedent(
"""\
#! /bin/sh
exec {runfile} "{cmd}" "$@"
""".format(
runfile=runfile,
cmd=cmd,
)
)
)
os.chmod(shim, stat.S_IRWXU)

os.symlink(target.format(command=cmd), os.path.join(shimdir, name))
# Container commands
container_excludes = (
build_config["container"].get("commands", {}).get("exclude", [])
)
for cmd, shim in glob_commands(container_excludes):
os.symlink(cmd, shim)

add_commands(nopyrex_globs, "{command}")
add_commands(command_globs, "exec-shim-pyrex")
container_includes = (
build_config["container"].get("commands", {}).get("include", [])
)
for _, shim in glob_commands(container_includes):
os.symlink("exec-shim-pyrex", shim)

return shimdir

Expand Down