How do I define a rule that combines a target built for 2 different CPU architectures into one package ?

737 views
Skip to first unread message

sarv...@gmail.com

unread,
Sep 4, 2019, 6:51:03 PM9/4/19
to bazel-discuss

This is question on how to combine a multiplatform binary that can build say an arm and x86 binary with say a rule like the following
cc_library(
    name = "multiplatform_lib",
    srcs = select({
        ":x86_mode": ["x86_impl.cc"],
        ":arm_mode": ["arm_impl.cc"]
    })
)
config_setting(
    name = "x86_mode",
    values = { "cpu": "x86" }
)
config_setting(
    name = "arm_mode",
    values = { "cpu": "arm" }
)

And I then want to implement a packaging rule that combines the binaries for the above 2 architectures(arm_mode and x86_mode), into single directory and tar them into 1 package "myconsdolidated.tar.gz", 
How Can I achieve that ?

mypackage_rule(
    name = "myconsolidated.tar.gz",
    srcs = [":multiplatform_lib x86_mode", ":multiplatform_lib arm_mode"]
)

Alex Humesky

unread,
Sep 5, 2019, 5:52:51 PM9/5/19
to sarv...@gmail.com, Greg Estren, Julie Xia, bazel-discuss
Something like this should be close to what you're looking for:

defs.bzl:

def _multi_cpu_package_impl(ctx):

  zipper_args = ["c", ctx.outputs.zip.path]
  binaries = {}
  for dep in ctx.attr.deps:
    files = dep.files.to_list()
    if "bazel-out/k8" in files[0].path:
      binaries["k8"] = files
    elif "bazel-out/armeabi-v7a" in files[0].path:
      binaries["armeabi-v7a"] = files

  zipper_inputs = []
  for cpu, files in binaries.items():
    for f in files:
      zipper_args.append("%s/%s=%s" % (cpu, f.short_path, f.path))
      zipper_inputs.append(f)

  ctx.actions.run(
    inputs = zipper_inputs,
    outputs = [ctx.outputs.zip],
    executable = ctx.executable._zipper,
    arguments = zipper_args,
    progress_message = "Creating zip...",
    mnemonic = "zipper",
  )

def _multi_cpu_transition_impl(settings, attr):
  return {
    "k8": {
      "//command_line_option:cpu": "k8",
    },
    "armeabi-v7a": {
      "//command_line_option:cpu": "armeabi-v7a",
    }
  }

_multi_cpu_transition = transition(
  implementation = _multi_cpu_transition_impl,
  inputs = [],
  outputs = [
    "//command_line_option:cpu",
  ],
)

multi_cpu_package = rule(
  implementation = _multi_cpu_package_impl,
  attrs = {
    "deps": attr.label_list(cfg = _multi_cpu_transition),
    "$zipper": attr.label(default = Label("@bazel_tools//tools/zip:zipper"), cfg = "host", executable=True),

    "_whitelist_function_transition": attr.label(default = "//tools/whitelists/function_transition_whitelist"),
  },
  outputs = {"zip": "%{name}.zip"},
)


tools/whitelists/function_transition_whitelist/BUILD

package_group(
  name = "function_transition_whitelist",
  packages = ["//..."],
)


BUILD:

load(":defs.bzl", "multi_cpu_package")

cc_binary(
  name = "foo",
  srcs = ["foo.c"],
)

multi_cpu_package(
  name = "foo_pkg",
  deps = [
    ":foo",
  ],
)


foo.c:

int main() {
  return 0;
}


Some notes:

1. In order to distinguish the targets in ctx.attr.deps by their configuration, this example examines the paths of files in that target, because those paths happen to contain the cpu the files were built for. In earlier iterations of this API, there is ctx.split_attr.<attr> which would contain a map with the same keys as in the map that the transition implementation function returns (here, the keys are the cpu values we want), but that no longer seems to be populated (+Greg Estren +Julie Xia)

2. Out of convenience I used zipper, which is bundled with bazel, instead of tar.

3. The example collapses a bunch of depsets with to_list(), which is usually bad for performance, but using Args was tricky with zipper. This is a top-level rule so it's probably ok here.

4. The whitelist thing is necessary because some transitions can cause performance problems for large depots. In fact, in the case here, everything is analyzed and built twice (once for each cpu), which will result in up to about 2x memory usage (basically have to store the graph twice). There is ongoing work to make this better.

5. The example doesn't actually work out-of-the-box with bazel because bazel's crosstool contains a fake definition for the armeabi-v7a toolchain. You'll want to adjust the values in _multi_cpu_transition_impl to match your crosstool.


--
You received this message because you are subscribed to the Google Groups "bazel-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bazel-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/bazel-discuss/6507c0fc-bef3-4931-9712-4ef70022647a%40googlegroups.com.

Alex Humesky

unread,
Sep 5, 2019, 5:58:27 PM9/5/19
to sarv...@gmail.com, Greg Estren, Julie Xia, bazel-discuss
Also, here are the example files
multi_cpu_package_example.zip

sarv...@gmail.com

unread,
Sep 6, 2019, 6:16:25 PM9/6/19
to bazel-discuss
Thanks that helps.
To unsubscribe from this group and stop receiving emails from it, send an email to bazel-...@googlegroups.com.

sarv...@gmail.com

unread,
Sep 9, 2019, 1:13:12 AM9/9/19
to bazel-discuss
Quick followup. 
The code clearsup how I could define a package rule that packages all the package variants of pkg1 into into a super package.
How I allow for selecting which variants I want from the rule invocation.

For example:
   if //pkg1:lib1 is built for ppc, arm & x86, and also featureset feat1 and feat2, for example:
And I want to create a super package target, say
superpkg(
    name = '//pkg1:supertarget1',  
    deps = [':lib1 ppc feat1', ':lib1 arm feat1', ':lib1 x86 feat1']
)
superpkg(
    name='//pkg1:supertarget2', 
    deps = [':lib1 ppc feat2', ':lib1 arm feat2']
)

Asked another way, how do specify the dependent targets by package:name as well cpu/arch/feat ?
since, deps are label_lists, and they seem to only specify dependencies by "package:target" dont seem to be capable of being more specific about feature/arch variant.

Thanks,
Sarvi

Alex Humesky

unread,
Sep 10, 2019, 9:25:19 PM9/10/19
to sarv...@gmail.com, Greg Estren, bazel-discuss
For that you would use Starlark Build Configuration:

It would look something like this, however there's a missing piece I'm not sure of:

BUILD:

cc_library(
  name = "bar",
  hdrs = ["bar.h"] + select({
    ":enable_featureA@enabled": ["featureA.h"],
    ":enable_featureA@disabled": [],
    "//conditions:default": [],
  }),
  srcs = ["bar.c"] + select({
    ":enable_featureA@enabled": ["featureA.c"],
    ":enable_featureA@disabled": [],
    "//conditions:default": [],
  }),
)

bool_flag(
  name = "enable_featureA",
  build_setting_default = False,
)

config_setting(
  name = "enable_featureA@enabled",
  flag_values = {":enable_featureA": "True"},
)

config_setting(
  name = "enable_featureA@disabled",
  flag_values = {":enable_featureA": "False"},
)


and then the Starlark transition looks like this:

defs.bzl;

def _multi_cpu_transition_impl(settings, attr):
  return {
    "k8": {
      "//command_line_option:cpu": "k8",
      "//:enable_featureA": False,
    },
    "k8-featureA": {
      "//command_line_option:cpu": "k8",
      "//:enable_featureA": True,
    },
    # etc

  }

_multi_cpu_transition = transition(
  implementation = _multi_cpu_transition_impl,
  inputs = [],
  outputs = [
    "//command_line_option:cpu",
    "//:enable_featureA",
  ],
)


However, the missing piece is how to access the configuration information of the different versions of each dep in the package rule implementation. In the original example, the CPU happened to be in path of the files, so we could infer which configuration that version of the dep comes from, but here, the CPU remains the same. Maybe +Greg Estren knows.

To unsubscribe from this group and stop receiving emails from it, send an email to bazel-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/bazel-discuss/dfd97b11-2875-4593-83c0-f8eabea880a9%40googlegroups.com.

Greg Estren

unread,
Sep 11, 2019, 5:42:28 PM9/11/19
to Alex Humesky, sarv...@gmail.com, bazel-discuss
The interface for that (figuring out which split path a dep came from) is reasonably-well thought out. But the implementation remains a TODO, mostly pending on real-world need we haven't heard a call for yet. We can prioritize accordingly based on user feedback.

sarv...@gmail.com

unread,
Sep 12, 2019, 10:24:12 PM9/12/19
to bazel-discuss
Packaging for a multi-cpu system is the perfect real world example here.
Or for that matter even packaging a multi featureset library, even within a single cpu scenario, is real world example.

The simplest use case I can thiink of, is say building and packaging  libA, which can be built with 3 different capabilities.

In the multi featureset rul example, Alex specified, above
cc_library(
  name = "bar",
  hdrs = ["bar.h"] + select({
    ":enable_featureA@enabled": ["featureA.h"],
    ":enable_featureA@disabled": [],
    "//conditions:default": [],
  }),....................

I can now build "bar" with featureA enabled or disabled.

In my own custom packaging rule I create, I now now how I can use transitions to go across CPU architectures, though exactly not clear(some clarity would be helpfull), I suspect I can implement the starlark build configuration that I can pick the right featured version of the package.

What I don't see is how to specify the deps=[] field where I can specify the variants.
package_rule(
  name = "mypackage",
    deps =':bar????????"
)

Though being able specify the variants in "deps" might be useful. The more likely case is I specific a "specfile" attribute to the rule,  
package_rule(
  name = "mypackage",
    specs ='myoackage.spec"
)
Where mypackage.spec has the definition of what should be packaged, bar:featureA@enabled OR bar:featureA:disabled, 

This is assuming I can read and parse the content of "mypackage.spec"  and translate that to a list feature/arch specific config transitions in the starlark language.

I am still playing with the starlark langauge and not sure if the above is possible?

But the first question I had was, how can I "specify/describe" these config transitions in my label list attributes
 

Thanks,
Sarvi

Alex Humesky

unread,
Sep 12, 2019, 11:01:51 PM9/12/19
to sarv...@gmail.com, bazel-discuss
On Thu, Sep 12, 2019 at 10:24 PM <sarv...@gmail.com> wrote:
Packaging for a multi-cpu system is the perfect real world example here.
Or for that matter even packaging a multi featureset library, even within a single cpu scenario, is real world example.

The simplest use case I can thiink of, is say building and packaging  libA, which can be built with 3 different capabilities.

In the multi featureset rul example, Alex specified, above
cc_library(
  name = "bar",
  hdrs = ["bar.h"] + select({
    ":enable_featureA@enabled": ["featureA.h"],
    ":enable_featureA@disabled": [],
    "//conditions:default": [],
  }),....................

I can now build "bar" with featureA enabled or disabled.

In my own custom packaging rule I create, I now now how I can use transitions to go across CPU architectures, though exactly not clear(some clarity would be helpfull), I suspect I can implement the starlark build configuration that I can pick the right featured version of the package.

What I don't see is how to specify the deps=[] field where I can specify the variants.
package_rule(
  name = "mypackage",
    deps =':bar????????"
)

The transition function receives the attributes of the instance of the rule it's being evaluated with, so you can add an attribute, say, "enabled_features":

multi_cpu_package = rule(
  implementation = _multi_cpu_package_impl,
  attrs = {
    "deps": attr.label_list(cfg = _multi_cpu_transition),
    "$zipper": attr.label(default = Label("@bazel_tools//tools/zip:zipper"), cfg = "host", executable=True),
    # note that there is already a built-in attribute named "features"
    "enabled_features": attr.string_list(),


    "_whitelist_function_transition": attr.label(default = "//tools/whitelists/function_transition_whitelist"),
  },
  outputs = {"zip": "%{name}.zip"},
)


and then read that in the transition function:

def _multi_cpu_transition_impl(settings, attr):
  enable_a = "a" in attr.enabled_features

  return {
    "k8": {
      "//command_line_option:cpu": "k8",
      "//:enable_featureA": enable_a,
    },
    "arm": {
      "//command_line_option:cpu": "arm",
      "//:enable_featureA": enable_a,
    },
  }


So, it's not possible to configure targets within the list of deps individually. If you had libA and libB in deps, featureA couldn't be enabled for one and not the other. (Such a feature has been proposed, I believe.) To do that, libA and libB would need different "feature names", so that they can be toggled separately at the top level. Another way to do this would be to introduce additional intermediate rules with transitions, so that the attrs can be configured independently at that level:

package(name = "pkg", deps = ["a", "b"])
intermediate(name = "a", deps = ["libA"], enabled_features = ["featureA"])
intermediate(name = "b", deps = ["libB"], enabled_features = []) # not A
cc_library(name = "libA", srcs = select({"featureA": [....], ....}))
cc_library(name = "libB", srcs = select({"featureA": [....], ....}))

This might also enable working around the interface for getting the configuration information not being implemented yet, since the intermediate rules can read their attributes and propagate that information in providers, which the top-level rule can read.


Though being able specify the variants in "deps" might be useful. The more likely case is I specific a "specfile" attribute to the rule,  
package_rule(
  name = "mypackage",
    specs ='myoackage.spec"
)
Where mypackage.spec has the definition of what should be packaged, bar:featureA@enabled OR bar:featureA:disabled, 

This is assuming I can read and parse the content of "mypackage.spec"  and translate that to a list feature/arch specific config transitions in the starlark language.

I am still playing with the starlark langauge and not sure if the above is possible?

You're correct: starlark can't read files.
One way to work around this is to write the spec in a separate bzl file, and interpret that as python elsewhere where you need it. That's kind of fragile though.
 
To unsubscribe from this group and stop receiving emails from it, send an email to bazel-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/bazel-discuss/a7563298-eca6-403f-9552-966a77456152%40googlegroups.com.

ahum...@google.com

unread,
Feb 18, 2022, 5:27:34 PM2/18/22
to bazel-discuss
I know this is an old thread, but this same topic came up elsewhere and I posted a workaround that fills in the "missing piece" I mentioned above:
Reply all
Reply to author
Forward
0 new messages