Skip to content

Commit

Permalink
fix: make Python repositories chmod/id/touch hermetic
Browse files Browse the repository at this point in the history
  • Loading branch information
mattyclarkson committed Jul 1, 2024
1 parent 084b877 commit 7a71c56
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 3 deletions.
77 changes: 77 additions & 0 deletions python/private/chmod.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#! /usr/bin/env python3

# Copyright 2023 The Bazel Authors. All rights reserved.
#
# 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.

"""
Provides a `chmod` implementation that recursively removes write permissions.
"""

from __future__ import annotations

from itertools import chain
from argparse import ArgumentParser
from pathlib import Path
from stat import S_IWGRP, S_IWOTH, S_IWUSR


def readonly(value: str) -> int:
if value != "ugo-w":
raise ValueError("Only `ugo-w` is supported")

return ~(S_IWUSR | S_IWGRP | S_IWOTH)


def directory(value: str) -> Path:
path = Path(value)

if not path.exists():
raise ValueError(f"`{path}` must exist")

if not path.is_dir():
raise ValueError("Must be a directory")

return path


def main():
parser = ArgumentParser(prog="chmod", description="Change file mode bits.")
parser.add_argument(
"-R",
"--recursive",
action="store_true",
help="Recursively set permissions.",
required=True,
)
parser.add_argument(
"mask",
metavar="MODE",
help="Symbolic mode settings.",
type=readonly,
)
parser.add_argument(
"directory",
metavar="FILE",
help="Filepath(s) to operate on.",
type=directory,
)
args = parser.parse_args()

for path in chain((args.directory,), args.directory.glob("**/*")):
stat = path.stat()
path.chmod(stat.st_mode & args.mask)


if __name__ == "__main__":
main()
45 changes: 45 additions & 0 deletions python/private/id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#! /usr/bin/env python3

# Copyright 2023 The Bazel Authors. All rights reserved.
#
# 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.

"""
Provides an `id` implementation.
"""

from __future__ import annotations

from argparse import ArgumentParser
from os import getuid


def main():
parser = ArgumentParser(
prog="id", description="Print real and effective user and group IDs."
)
parser.add_argument(
"-u",
"--user",
help="Print only the effective user ID.",
action="store_true",
required=True,
)
args = parser.parse_args()
assert args.user

print(getuid())


if __name__ == "__main__":
main()
43 changes: 43 additions & 0 deletions python/private/touch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#! /usr/bin/env python3

# Copyright 2023 The Bazel Authors. All rights reserved.
#
# 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.

"""
Provides a `touch` implementation.
"""

from __future__ import annotations

from argparse import ArgumentParser
from pathlib import Path


def main():
parser = ArgumentParser(prog="touch", description="Change file timestamps.")
parser.add_argument(
"files",
metavar="FILE",
help="Filepath(s) to operate on.",
type=Path,
nargs="+",
)
args = parser.parse_args()

for path in args.files:
path.touch()


if __name__ == "__main__":
main()
37 changes: 34 additions & 3 deletions python/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ load("//python/private:internal_config_repo.bzl", "internal_config_repo")
load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
load(
"//python/private:toolchains_repo.bzl",
"get_host_os_arch",
"get_host_platform",
"host_toolchain",
"multi_toolchain_aliases",
"toolchain_aliases",
Expand Down Expand Up @@ -104,6 +106,24 @@ def is_standalone_interpreter(rctx, python_interpreter_path):
],
).return_code == 0

def _get_host_python_interpreter(rctx):
(os_name, arch) = get_host_os_arch(rctx)
host_platform = get_host_platform(os_name, arch)
platform = rctx.attr.platform
python = "python.exe" if ("windows" in platform) else "bin/python3"

if platform == host_platform:
return python

label = "@@{suffix}~{repo}_{host_platform}//:{python}".format(
suffix = rctx.attr.name.rsplit("~python", 1)[0],
repo = "python_3_11", # Aligns with what is defined in `MODULE.bazel`
host_platform = host_platform,
python = python,
)

return Label(label)

def _python_repository_impl(rctx):
if rctx.attr.distutils and rctx.attr.distutils_content:
fail("Only one of (distutils, distutils_content) should be set.")
Expand Down Expand Up @@ -193,15 +213,17 @@ def _python_repository_impl(rctx):
if "windows" not in platform:
lib_dir = "lib" if "windows" not in platform else "Lib"

python = _get_host_python_interpreter(rctx)

repo_utils.execute_checked(
rctx,
op = "python_repository.MakeReadOnly",
arguments = [repo_utils.which_checked(rctx, "chmod"), "-R", "ugo-w", lib_dir],
arguments = [python, "-B", rctx.attr._chmod, "-R", "ugo-w", lib_dir],
)
exec_result = repo_utils.execute_unchecked(
rctx,
op = "python_repository.TestReadOnly",
arguments = [repo_utils.which_checked(rctx, "touch"), "{}/.test".format(lib_dir)],
arguments = [python, rctx.attr._touch, "{}/.test".format(lib_dir)],
)

# The issue with running as root is the installation is no longer
Expand All @@ -210,7 +232,7 @@ def _python_repository_impl(rctx):
stdout = repo_utils.execute_checked_stdout(
rctx,
op = "python_repository.GetUserId",
arguments = [repo_utils.which_checked(rctx, "id"), "-u"],
arguments = [python, rctx.attr._id, "-u"],
)
uid = int(stdout.strip())
if uid == 0:
Expand Down Expand Up @@ -529,6 +551,15 @@ For more information see the official bazel docs
"zstd_version": attr.string(
default = "1.5.2",
),
"_chmod": attr.label(
default = "//python/private:chmod.py",
),
"_touch": attr.label(
default = "//python/private:touch.py",
),
"_id": attr.label(
default = "//python/private:id.py",
),
},
environ = [REPO_DEBUG_ENV_VAR],
)
Expand Down

0 comments on commit 7a71c56

Please sign in to comment.