antlir/compiler/requires_provides.py (107 lines of code) (raw):
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
'''
Images are composed of a bunch of Items. These are declared by the user
in an order-independent fashion, but they have to be installed in a specific
order. For example, we can only copy a file into a directory after the
directory already exists.
The main jobs of the image compiler are:
- to validate that the specified Items will work well together, and
- to install them in the appropriate order.
To do these jobs, each Item Provides certain filesystem features --
described in this file -- and also Requires certain predicates about
filesystem features -- described in `requires.py`.
Requires and Provides must interact in some way -- either
(1) Provides objects need to know when they satisfy each requirements, or
(2) Requires objects must know all the Provides that satisfy them.
The first arrangement seemed more maintainable, so each Provides object has
to define its relationship with every Requires predicate, thus:
def matches(self, path_to_reqs_provs, predicate):
"""
`path_to_reqs_provs` is the map constructed by `ValidatedReqsProvs`.
This is a breadcrumb for the future -- having the full set of
"provides" objects will let us resolve symlinks.
"""
return True or False
Future: we might want to add permissions constraints, tackle following
symlinks (or not following them), maybe hardlinks, etc. This would
likely best be tackled via predicate composition with And/Or/Not support
with short-circuiting. E.g. FollowsSymlinks(Pred) would expand to:
Or(
And(IsSymlink(Path), Pred(SymlinkTarget(Path))),
And(Not(IsSymlink(Path)), Pred(SymlinkTarget(Path)),
)
'''
import dataclasses
from enum import Enum, auto
from antlir.fs_utils import Path
class RequirementKind(Enum):
PATH = auto()
GROUP = auto()
USER = auto()
@dataclasses.dataclass(frozen=True)
class Requirement:
kind: RequirementKind
def _normalize_path(path: Path) -> Path:
# Normalize paths as image-absolute. This is crucial since we
# will use `path` as a dictionary key.
return Path(b"/" / path.strip_leading_slashes()).normpath()
@dataclasses.dataclass(frozen=True)
# pyre-fixme[13]: Attribute `path` is never initialized.
class RequirePath(Requirement):
path: Path
def __init__(self, path: Path) -> None:
super().__init__(kind=RequirementKind.PATH)
object.__setattr__(self, "path", _normalize_path(path))
class RequireDirectory(RequirePath):
pass
class RequireFile(RequirePath):
pass
class _RequireDoNotAccess(RequirePath):
# Only ProvidesDoNotAccess should instantiate this type of RequirePath and
# it is meant to fail compilation if a RequireDirectory or RequireFile is
# requested at this path.
pass
@dataclasses.dataclass(frozen=True)
# pyre-fixme[13]: Attribute `target` is never initialized.
class RequireSymlink(RequirePath):
target: Path
def __init__(self, path: Path, target: Path) -> None:
super().__init__(path=path)
object.__setattr__(self, "target", target)
@dataclasses.dataclass(frozen=True)
# pyre-fixme[13]: Attribute `name` is never initialized.
class RequireGroup(Requirement):
name: str
def __init__(self, name: str) -> None:
super().__init__(kind=RequirementKind.GROUP)
object.__setattr__(self, "name", name)
@dataclasses.dataclass(frozen=True)
# pyre-fixme[13]: Attribute `name` is never initialized.
class RequireUser(Requirement):
name: str
def __init__(self, name: str) -> None:
super().__init__(kind=RequirementKind.USER)
object.__setattr__(self, "name", name)
@dataclasses.dataclass(frozen=True)
class Provider:
req: Requirement
def provides(self, req: Requirement) -> bool:
return self.req == req
@dataclasses.dataclass(frozen=True)
class ProvidesPath(Provider):
req: RequirePath
def path(self) -> Path:
return self.req.path
def with_new_path(self, new_path: Path) -> "ProvidesPath":
# pyre-fixme[6]: Expected `RequirePath` for 1st param but got `Path`.
return self.__class__(new_path)
class ProvidesDirectory(ProvidesPath):
def __init__(self, path: Path) -> None:
super().__init__(req=RequireDirectory(path=path))
class ProvidesFile(ProvidesPath):
"Does not have to be a regular file, just any leaf in the FS tree"
def __init__(self, path: Path) -> None:
super().__init__(req=RequireFile(path=path))
class ProvidesSymlink(ProvidesPath):
def __init__(self, path: Path, target: Path) -> None:
super().__init__(req=RequireSymlink(path, target))
def with_new_path(self, new_path: Path) -> "ProvidesSymlink":
# pyre-fixme[16]: `RequirePath` has no attribute `target`.
return self.__class__(new_path, self.req.target)
class ProvidesDoNotAccess(ProvidesPath):
def __init__(self, path: Path) -> None:
super().__init__(req=_RequireDoNotAccess(path=path))
class ProvidesGroup(Provider):
def __init__(self, groupname: str) -> None:
super().__init__(req=RequireGroup(groupname))
class ProvidesUser(Provider):
def __init__(self, username: str) -> None:
super().__init__(req=RequireUser(username))