From b93764a51a6076ae08a18da8accd48016e1188c0 Mon Sep 17 00:00:00 2001 From: Vinnie Magro Date: Fri, 20 Oct 2023 15:08:16 -0700 Subject: [PATCH] [antlir2][buck] new configuration settings for flavor/os MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Add some buck2 configuration constraints for OS version. This diff by itself enables `select()` based on these constraints, but does not yet plumb it through the rest of the stack to make it consistent with `flavor`. See the next diff for that implementation, more Summary details and staticdocs. Test Plan: ``` ❯ buck2 test fbcode//antlir/antlir2/test_images/cfg/os: Buck UI: https://www.internalfb.com/buck2/ce167cd0-920b-4406-9ed1-49b6a64132cc Test UI: https://www.internalfb.com/intern/testinfra/testrun/8725724465341791 Network: Up: 0B Down: 101MiB (reSessionID-3ba4921a-dc3f-4354-82d8-12b813ff7e5a) Jobs completed: 604596. Time elapsed: 39.6s. Tests finished: Pass 18. Fail 0. Fatal 0. Skip 0. Build failure 0 ``` Reviewed By: epilatow Differential Revision: D49553996 fbshipit-source-id: c8d219a3e7897d8cecafc8b822ec7a001f28dff6 --- antlir/antlir2/bzl/image/cfg.bzl | 71 +++++++++++++++++++-- antlir/antlir2/bzl/image/layer.bzl | 21 ++++-- antlir/antlir2/os/BUCK | 27 ++++++++ antlir/antlir2/os/defs.bzl | 53 ++++++++++++++++ antlir/antlir2/os/family/BUCK | 25 ++++++++ antlir/antlir2/os/package.bzl | 26 ++++++++ antlir/antlir2/test_images/cfg/os/BUCK | 74 ++++++++++++++++++++++ antlir/antlir2/test_images/cfg/os/defs.bzl | 16 +++++ antlir/antlir2/test_images/cfg/os/test.rs | 62 ++++++++++++++++++ antlir/antlir2/testing/image_test.bzl | 7 +- 10 files changed, 370 insertions(+), 12 deletions(-) create mode 100644 antlir/antlir2/os/BUCK create mode 100644 antlir/antlir2/os/defs.bzl create mode 100644 antlir/antlir2/os/family/BUCK create mode 100644 antlir/antlir2/os/package.bzl create mode 100644 antlir/antlir2/test_images/cfg/os/BUCK create mode 100644 antlir/antlir2/test_images/cfg/os/defs.bzl create mode 100644 antlir/antlir2/test_images/cfg/os/test.rs diff --git a/antlir/antlir2/bzl/image/cfg.bzl b/antlir/antlir2/bzl/image/cfg.bzl index 1c11a48baf5..6bea128ce72 100644 --- a/antlir/antlir2/bzl/image/cfg.bzl +++ b/antlir/antlir2/bzl/image/cfg.bzl @@ -12,8 +12,25 @@ Currently this supports reconfiguring the target cpu architecture. """ load("//antlir/antlir2/bzl/image/facebook:fb_cfg.bzl", "fbcode_platform_refs", "transition_fbcode_platform") +load("//antlir/antlir2/os:defs.bzl", "OsVersionInfo") load("//antlir/bzl:build_defs.bzl", "is_facebook") +def cfg_attrs(): + return { + "default_os": attrs.option(attrs.string(), default = None, doc = """ + Reconfigure the layer when no antlir2 os has been set yet, so that + each intermediate layer can be passed to `buck build` and give a + reasonable default. + For more details, see: + https://www.internalfb.com/intern/staticdocs/antlir2/docs/recipes/multi-os-images/ + """), + "target_arch": attrs.option( + attrs.enum(["x86_64", "aarch64"]), + default = None, + doc = "Build this image for a specific target arch without using `buck -c`", + ), + } + def _impl(platform: PlatformInfo, refs: struct, attrs: struct) -> PlatformInfo: constraints = platform.configuration.constraints @@ -23,6 +40,26 @@ def _impl(platform: PlatformInfo, refs: struct, attrs: struct) -> PlatformInfo: if is_facebook: constraints = transition_fbcode_platform(refs, attrs, constraints) + if attrs.default_os: + os = getattr(refs, "os." + attrs.default_os)[OsVersionInfo] + os_constraint = os.constraint[ConstraintValueInfo] + family = os.family[ConstraintValueInfo] + + # The rule transition to set the default antlir2 OS only happens if the + # target has not been configured for a specific OS yet. This way the dep + # transition takes precedence - in other words, the default_os attribute of + # the leaf image being built is always respected and reconfigures all layers + # along the parent_layer chain + if os_constraint.setting.label not in constraints: + constraints[os_constraint.setting.label] = os_constraint + constraints[family.setting.label] = family + + # If a build appliance is being built, we must remove the OS configuration + # constraint to avoid circular dependencies. + if attrs.antlir_internal_build_appliance: + constraints.pop(refs.os_constraint[ConstraintSettingInfo].label, None) + constraints.pop(refs.os_family_constraint[ConstraintSettingInfo].label, None) + label = platform.label # if we made any changes, change the label @@ -42,13 +79,39 @@ layer_cfg = transition( refs = { "arch.aarch64": "ovr_config//cpu/constraints:arm64", "arch.x86_64": "ovr_config//cpu/constraints:x86_64", + "os.centos8": "//antlir/antlir2/os:centos8", + "os.centos9": "//antlir/antlir2/os:centos9", + "os.eln": "//antlir/antlir2/os:eln", + "os.none": "//antlir/antlir2/os:none", + "os_constraint": "//antlir/antlir2/os:os", + "os_family_constraint": "//antlir/antlir2/os/family:family", } | ( # @oss-disable # @oss-enable {} ), - attrs = [ - # target_arch on image.layer is read to reconfigure for the target cpu - # arch without having to use -c fbcode.arch - "target_arch", + attrs = cfg_attrs().keys() + [ + # Build appliances are very low level and cannot depend on a flavor, so + # they are just not transitioned to an os configuration + "antlir_internal_build_appliance", ], ) + +def _remove_os_impl(platform: PlatformInfo, refs: struct) -> PlatformInfo: + constraints = platform.configuration.constraints + constraints.pop(refs.os_constraint[ConstraintSettingInfo].label, None) + constraints.pop(refs.os_family_constraint[ConstraintSettingInfo].label, None) + return PlatformInfo( + label = platform.label, + configuration = ConfigurationInfo( + constraints = constraints, + values = platform.configuration.values, + ), + ) + +remove_os_constraint = transition( + impl = _remove_os_impl, + refs = { + "os_constraint": "//antlir/antlir2/os:os", + "os_family_constraint": "//antlir/antlir2/os/family:family", + }, +) diff --git a/antlir/antlir2/bzl/image/layer.bzl b/antlir/antlir2/bzl/image/layer.bzl index 53333e86e7b..551e7960edb 100644 --- a/antlir/antlir2/bzl/image/layer.bzl +++ b/antlir/antlir2/bzl/image/layer.bzl @@ -17,13 +17,14 @@ load( ) # @oss-disable # @oss-disable +load("//antlir/antlir2/os:package.bzl", "get_default_os_for_package", "should_all_images_in_package_use_default_os") load("//antlir/bzl:build_defs.bzl", "alias", "is_facebook") load("//antlir/bzl:constants.bzl", "REPO_CFG") load("//antlir/bzl:types.bzl", "types") load("//antlir/rpm/dnf2buck:repo.bzl", "RepoInfo", "RepoSetInfo") # @oss-disable load("//antlir/bzl/build_defs.bzl", "config", "get_visibility") -load(":cfg.bzl", "layer_cfg") +load(":cfg.bzl", "cfg_attrs", "layer_cfg", "remove_os_constraint") load(":depgraph.bzl", "build_depgraph") load(":mounts.bzl", "all_mounts", "container_mount_args") @@ -451,7 +452,7 @@ _layer_attrs = { "antlir2": attrs.exec_dep(default = "//antlir/antlir2/antlir2:antlir2"), "antlir_internal_build_appliance": attrs.bool(default = False, doc = "mark if this image is a build appliance and is allowed to not have a flavor"), "build_appliance": attrs.option( - attrs.dep(providers = [LayerInfo]), + attrs.transition_dep(providers = [LayerInfo], cfg = remove_os_constraint), default = None, ), "default_mountpoint": attrs.option(attrs.string(), default = None), @@ -497,11 +498,6 @@ _layer_attrs = { attrs.dep(providers = [LayerInfo]), default = None, ), - "target_arch": attrs.option( - attrs.enum(["x86_64", "aarch64"]), - default = None, - doc = "Build this image for a specific target arch", - ), "_implicit_image_test": attrs.option( attrs.exec_dep(providers = [ExternalRunnerTestInfo]), default = None, @@ -515,6 +511,8 @@ _layer_attrs = { )), } +_layer_attrs.update(cfg_attrs()) + _layer_attrs.update( { "_feature_" + key: val @@ -537,6 +535,9 @@ def layer( # by a type hint inside feature.bzl. Feature targets or # InlineFeatureInfo providers are accepted, at any level of nesting features = [], + default_os: str | None = None, + # TODO: remove this flag when all images are using this new mechanism + use_default_os_from_package: bool | None = None, # We'll implicitly forward some users to antlir2, so any hacks for them # should be confined behind this flag implicit_antlir2: bool = False, @@ -551,6 +552,11 @@ def layer( flavor = kwargs.pop("flavor", None) kwargs["flavor"] = compat.from_antlir1_flavor(flavor) if flavor else None + if use_default_os_from_package == None: + use_default_os_from_package = should_all_images_in_package_use_default_os() + if use_default_os_from_package: + default_os = default_os or get_default_os_for_package() + kwargs.update({"_feature_" + key: val for key, val in feature_attrs(features).items()}) if is_facebook: @@ -579,6 +585,7 @@ def layer( return layer_rule( name = name, + default_os = default_os, visibility = get_visibility(visibility), _implicit_image_test = "//antlir/antlir2/testing/implicit_image_test:implicit_image_test", **kwargs diff --git a/antlir/antlir2/os/BUCK b/antlir/antlir2/os/BUCK new file mode 100644 index 00000000000..f561e66ed23 --- /dev/null +++ b/antlir/antlir2/os/BUCK @@ -0,0 +1,27 @@ +load("@prelude//:rules.bzl", "constraint_setting") +load(":defs.bzl", "os_version") + +constraint_setting( + name = "os", + visibility = ["PUBLIC"], +) + +os_version( + name = "none", + family = "//antlir/antlir2/os/family:none", +) + +os_version( + name = "centos8", + family = "//antlir/antlir2/os/family:centos", +) + +os_version( + name = "centos9", + family = "//antlir/antlir2/os/family:centos", +) + +os_version( + name = "eln", + family = "//antlir/antlir2/os/family:fedora", +) diff --git a/antlir/antlir2/os/defs.bzl b/antlir/antlir2/os/defs.bzl new file mode 100644 index 00000000000..eadca0147f7 --- /dev/null +++ b/antlir/antlir2/os/defs.bzl @@ -0,0 +1,53 @@ +# 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. + +load("@prelude//:rules.bzl", "config_setting", "constraint_value") + +OsVersionInfo = provider(fields = [ + "constraint", + "family", +]) + +def _os_version_rule_impl(ctx: AnalysisContext) -> list[Provider]: + return [ + DefaultInfo(), + ctx.attrs.config_setting[ConfigurationInfo], + OsVersionInfo( + constraint = ctx.attrs.constraint, + family = ctx.attrs.family, + ), + ] + +_os_version_rule = rule( + impl = _os_version_rule_impl, + attrs = { + "config_setting": attrs.dep(providers = [ConfigurationInfo]), + "constraint": attrs.dep(providers = [ConstraintValueInfo]), + "family": attrs.dep(providers = [ConstraintValueInfo]), + }, +) + +def os_version( + name: str, + family: str): + constraint_value( + name = name + ".constraint", + constraint_setting = "//antlir/antlir2/os:os", + visibility = [":" + name], + ) + config_setting( + name = name + ".config", + constraint_values = [ + ":{}.constraint".format(name), + family, + ], + visibility = ["PUBLIC"], + ) + _os_version_rule( + name = name, + family = family, + constraint = ":{}.constraint".format(name), + config_setting = ":{}.config".format(name), + ) diff --git a/antlir/antlir2/os/family/BUCK b/antlir/antlir2/os/family/BUCK new file mode 100644 index 00000000000..04a389d52a5 --- /dev/null +++ b/antlir/antlir2/os/family/BUCK @@ -0,0 +1,25 @@ +load("@prelude//:rules.bzl", "constraint_setting", "constraint_value") + +constraint_setting( + name = "family", + visibility = ["PUBLIC"], +) + +constraint_value( + name = "none", + constraint_setting = ":family", + visibility = ["PUBLIC"], +) + +# Any version of CentOS. +constraint_value( + name = "centos", + constraint_setting = ":family", + visibility = ["PUBLIC"], +) + +constraint_value( + name = "fedora", + constraint_setting = ":family", + visibility = ["PUBLIC"], +) diff --git a/antlir/antlir2/os/package.bzl b/antlir/antlir2/os/package.bzl new file mode 100644 index 00000000000..75cd72b36b7 --- /dev/null +++ b/antlir/antlir2/os/package.bzl @@ -0,0 +1,26 @@ +# 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. + +def set_default_os_for_package(*, default_os: str): + write_package_value( + "antlir2.default_os", + default_os, + overwrite = True, + ) + +def get_default_os_for_package() -> str: + return read_package_value("antlir2.default_os") + +def all_images_in_package_use_default_os(yes: bool = True): + write_package_value( + "antlir2.all_images_in_package_use_default_os", + yes, + overwrite = True, + ) + +def should_all_images_in_package_use_default_os() -> bool: + return read_package_value( + "antlir2.all_images_in_package_use_default_os", + ) or False diff --git a/antlir/antlir2/test_images/cfg/os/BUCK b/antlir/antlir2/test_images/cfg/os/BUCK new file mode 100644 index 00000000000..ccb77985035 --- /dev/null +++ b/antlir/antlir2/test_images/cfg/os/BUCK @@ -0,0 +1,74 @@ +load("//antlir/antlir2/bzl/feature:defs.bzl", "feature") +load("//antlir/antlir2/bzl/image:defs.bzl", "image") +load("//antlir/antlir2/testing:image_test.bzl", "image_rust_test") +load(":defs.bzl", "write_os") + +oncall("twimage") + +image.layer( + name = "root", + features = [ + feature.rpms_install(rpms = [ + "bash", + "coreutils", + # @oss-disable + ]), + write_os("/root.os"), + ], + flavor = "//antlir/antlir2/test_images:test-image-flavor", +) + +image.layer( + name = "intermediate", + features = [ + write_os("/intermediate.os"), + ], + parent_layer = ":root", +) + +image.layer( + name = "leaf.generic", + features = [ + write_os("/leaf.os"), + ], + parent_layer = ":intermediate", +) + +[ + [ + image.layer( + name = "leaf." + os, + default_os = os, + features = [ + write_os("/leaf.os"), + ], + parent_layer = ":intermediate", + ), + image_rust_test( + name = "test-leaf." + os, + srcs = ["test.rs"], + crate = "test_leaf_" + os, + crate_root = "test.rs", + env = { + "OS": os, + }, + layer = ":leaf." + os, + ), + image_rust_test( + name = "test-leaf.generic." + os, + srcs = ["test.rs"], + crate = "test_leaf_generic_" + os, + crate_root = "test.rs", + default_os = os, + env = { + "OS": os, + }, + layer = ":leaf.generic", + ), + ] + for os in [ + "centos8", + "centos9", + "eln", + ] +] diff --git a/antlir/antlir2/test_images/cfg/os/defs.bzl b/antlir/antlir2/test_images/cfg/os/defs.bzl new file mode 100644 index 00000000000..42c38dabfe4 --- /dev/null +++ b/antlir/antlir2/test_images/cfg/os/defs.bzl @@ -0,0 +1,16 @@ +# 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. + +load("//antlir/antlir2/bzl/feature:defs.bzl", "feature") + +def write_os(path: str): + return feature.install_text( + dst = path, + text = select({ + "//antlir/antlir2/os:centos8": "centos8", + "//antlir/antlir2/os:centos9": "centos9", + "//antlir/antlir2/os:eln": "eln", + }), + ) diff --git a/antlir/antlir2/test_images/cfg/os/test.rs b/antlir/antlir2/test_images/cfg/os/test.rs new file mode 100644 index 00000000000..bd03a9fa255 --- /dev/null +++ b/antlir/antlir2/test_images/cfg/os/test.rs @@ -0,0 +1,62 @@ +/* + * 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. + */ + +use std::fmt::Display; +use std::str::FromStr; + +static LEVELS: &[&str] = &["root", "intermediate", "leaf"]; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum TestOs { + Centos(u32), + Eln, +} + +impl FromStr for TestOs { + type Err = String; + + fn from_str(s: &str) -> Result { + if let Some(num) = s.strip_prefix("centos") { + Ok(Self::Centos(num.parse().map_err(|e| { + format!("invalid number after centos: {}", e) + })?)) + } else if s == "eln" { + Ok(Self::Eln) + } else { + Err(format!("unsupported OS: {s}")) + } + } +} + +impl Display for TestOs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TestOs::Centos(v) => write!(f, "centos{}", v), + TestOs::Eln => write!(f, "eln"), + } + } +} + +fn os_from_env() -> TestOs { + std::env::var("OS") + .expect("OS env var missing") + .parse() + .expect("invalid OS env var") +} + +#[test] +fn os_select() { + let os = os_from_env().to_string(); + for level in LEVELS { + let filename = format!("/{level}.os"); + assert_eq!( + os, + std::fs::read_to_string(&filename) + .unwrap_or_else(|_| panic!("failed to read {filename}")), + ); + } +} diff --git a/antlir/antlir2/testing/image_test.bzl b/antlir/antlir2/testing/image_test.bzl index 87d033cb8a1..c6bc88fabd0 100644 --- a/antlir/antlir2/testing/image_test.bzl +++ b/antlir/antlir2/testing/image_test.bzl @@ -8,6 +8,7 @@ load("//antlir/antlir2/bzl:platform.bzl", "rule_with_default_target_platform") load("//antlir/antlir2/bzl:types.bzl", "LayerInfo") load("//antlir/antlir2/bzl/feature:defs.bzl", "feature") +load("//antlir/antlir2/bzl/image:cfg.bzl", "cfg_attrs", "layer_cfg") load("//antlir/antlir2/bzl/image:defs.bzl", "image") load("//antlir/bzl:build_defs.bzl", "add_test_framework_label", "buck_sh_test", "cpp_unittest", "python_unittest", "rust_unittest") load("//antlir/bzl:constants.bzl", "REPO_CFG") @@ -81,6 +82,7 @@ def _impl(ctx: AnalysisContext) -> list[Provider]: _image_test = rule( impl = _impl, attrs = { + "antlir_internal_build_appliance": attrs.default_only(attrs.bool(default = False), doc = "read by cfg.bzl"), "boot": attrs.bool( default = False, doc = "boot the container with /init as pid1 before running the test", @@ -112,8 +114,9 @@ _image_test = rule( "layer": attrs.dep(providers = [LayerInfo]), "run_as_user": attrs.string(default = "root"), "test": attrs.dep(providers = [ExternalRunnerTestInfo]), - }, + } | cfg_attrs(), doc = "Run a test inside an image layer", + cfg = layer_cfg, ) image_test = rule_with_default_target_platform(_image_test) @@ -133,6 +136,7 @@ def _implicit_image_test( boot_wants_units: [list[str], None] = None, hostname: str | None = None, _add_outer_labels: list[str] = [], + default_os: str | None = None, **kwargs): test_rule( name = name + "_image_test_inner", @@ -170,6 +174,7 @@ def _implicit_image_test( boot_after_units = boot_after_units, boot_wants_units = boot_wants_units, hostname = hostname, + default_os = default_os, ) image_cpp_test = partial(