Squashed 'third_party/rules_rust/' changes from 078c6908f..bf9ddeb7c

bf9ddeb7c Release 0.25.1 (#2049)
db5b2fd65 Update tinyjson (#2050)
6a7872ae3 Fix prost proto packages not sanitizing to valid module names (#2044)
c080d7bfa Moved legacy protobuf rules to `proto/protobuf` (#2043)
1281cc051 Remove debug code. (#2048)
cd126be1f Fix build failure finding crate_roots when mixed with generated sources (#2041)
7f751cddd Consolidate rust_prost_library and fix extension-only proto generation. (#2047)
6118c81f2 Release 0.25.0 (#2042)
a6f29fd07 Add Prost and Tonic rules. (#2033)
9442aed8c fix: `crate_type` more accurately corresponds to CC linking actions (#1975)
4f4e2b17b Re-enable zig example on CI (#2030)
2ded0c2f5 Fix flaky coverage test in CI (#2028)
36f8251f9 Exclude .tmp_git_root from globs (#1948)
ca750fa83 Eliminate Rustfmt action in Bindgen rules. Bindgen can run rustfmt (#2025)
c55ec0cfb Allow sysroots from cc_toolchains to be added to bindgen actions (#2024)
9314b1b0c Release 0.24.1 (#2023)
92ea74ade Making rust_std attr in rust_toolchain mandatory (#1984)
a54b8e14b Update `rust_library_group` to use `DepVariantInfo` (#2022)
47644346b Release v0.24.0 (#2020)
a6b0a7f39 Rust library group (#1848)
bc43f4841 Fix crate_universe's `all_crate_deps` and `aliases` functions failing in case the crate's Cargo.toml has condtional dependencies (#2018)
8f27ec7c5 fix: load cargo manifest without resolving abs path of deps (#2017)
23f99bb63 feature: `target_compatible_with` added to `CommonAttrs` (#1976)
11f8c9875 Make `rust_doc_test` inherit it's crate aliases attribute (#2007)
8e848414d Regenerated crate_universe outputs for all packages (#2011)
1b6365131 Don't use startup:windows (#2012)
e80582e75 Fix thumbv* platform resolution (#2010)
367f90ef0 Update bindgen version to 0.65.1 (#2008)
e6ed5bf90 Release 0.23.0 (#2003)
93b230bb8 Fix code coverage collection. (#2001)
0a14bfbb0 Minor CI and test cleanup (#2004)
3e2ee941a Update bindgen rules to build clang from source. (#1998)
5a1a7577d Split up cargo_build_script tests (#2002)
eb6413e83 Update various bash scripts to pipe errors to stderr (#1999)
affe947ac Update stardoc version (#1997)
7073146f8 Add support for armv8-m (#1993)
73a06f130 Added Rust 1.70.0 (#1991)
23c20a93f Fixes crates_vendor workspace name detection when using bzlmod (#1990)
f5813fa08 Set windows flags in platform-specific bazelrc (#1988)
c1632b5b5 Fix up anchor link (#1987)
56e760487 Fix typo in crate_universe-generated defs.bzl comment (#1981)
94cbe4c2c Symlink in the exec-root so that relative paths will work, unchanged. (#1781)
af8ef62eb Release 0.22.0 (#1974)
4aaa6de30 Allow specifying exec and target compatibility constraints (#1971)
f1b19c394 Update rules_apple in tests (#1972)
937e63399 Add T2 support for x86_64-unknown-none (#1967)
66b1bf165 fix: lld-link (MSVC) fix flags including `-l` prefix (#1958)
285dcbbb9 feature: expose `extra_rustc_flags` and `extra_exec_rustc_flags` at `rust_register_toolchains` (#1959)
0f25cb462 Removed `rust_toolchain.os` in favor of `rust_toolchain.exec_triple`. (#1960)
a2a1109dc Add T2 support for thumbv7em-none-eabi (#1957)
80f0eb488 Support for `no_std` mode (#1934)
99aaf0830 Rename crates_vendor_manifests to cvm to shorten windows path lengths (#1944)
0a57da049 Added tests for build script dependencies to crate_universe (#1943)
caffb0a08 Release 0.21.1 (#1936)
c869a17c7 Fix regression in building zlib (#1935)
24b9dea4f Release 0.21.0 (#1933)
7677c617e Add support for rustc flags to `rust_proto_library` (#1932)
fa304ae48 Updated zlib BUILD file to support darwin-arm64 (#1931)
a86313282 Added Rust 1.69.0 (#1930)
f0e12c707 Make BuildInfo provider public (#1920)
c6ad23aba Respect `#[global_allocator]` in `cc_common.link` builds (#1926)
d78752504 Exclude target directory from release tars (#1922)
0339cd18a [wasm-bindgen] Update to v0.2.84 (#1919)
07af5678e Handle corner case for windows architecture detection (#1915)
c56e7660d Fix optional deps by platform (#1911)
4663ff6a3 cc_common_link: also respect --custom_malloc if set (#1912)
dab425760 Add Rust 1.68.2 (#1908)
e4bd39f95 Add empty rustfmt.toml (#1907)
eaf513865 Support bzlmod (#1528)
1074ecbab Release v0.20.0 (#1900)
44aec0a79 ci: fix test config in cc_common_link_ubuntu2004 (#1904)
6571cde64 Adds per_crate_rustc_flag build setting. (#1827)
7a47449df Added Rust 1.68.1 (#1898)
e3bcdbad1 Fixed rustdoc warnings in crate_universe (#1897)
529f45900 Added `rustdoc_flags` attribute to rust_doc rule (#1867)
9e3499405 Have rustdoc return its output directory instead of zip default. (#1868)
9d6741f40 Implement support for optional crates enabled with dep: features (#1885)
fd10963ea Skip adding -lstatic to libtest and libstd on Darwin (#1620)
b3314b45e Release 0.19.1 (#1895)
c1a9fd86f Accumulate all features from cargo tree output (#1884)
206f71c95 Disable zig example (#1893)
1a5d07cd2 Add runfiles support to rust_stdlib_filegroup (#1890)
6996cd550 Deleted unused targets and cleanup docs (#1889)
a85e24e20 Fix triple constraints for iOS and watchOS (#1888)
e13fd3bad Release rules_rust and cargo-bazel (#1882)
9e9853d63 Add support for thumbv7em with hard float (#1871)
b3cd5962e Added Rust 1.68.0 (#1866)
f1b7aedf5 Support sparse indexes (#1857)
7f2dd433a Make fetch_shas work with mktemp from coreutils 8.32 (#1870)
a9cc01230 Update crate_universe dependencies (#1872)
c038e94ae Pipe stderr from cargo tree processes (#1879)
222d60322 Parallelize cargo tree calls (#1874)
cdbbf7131 Add Fuchsia platform support (#1833)
17e5b04c2 Use `_make_link_flags_darwin` when target os is `ios`. (#1843)
d9ecc8df4 crate_universe: Support fetching crates with git branch, tag or rev (#1846)
1c694cd60 Forward `toolchains` to `cargo_build_script` targets (#1862)
9affcbfa7 Skip detecting abi for empty values (#1830)
6193fe823 Re-enable crate_universe MacOS tests (#1861)
c25db95ae Updated Rust to 1.67.1 (#1864)
7b8fd06be Support `[patch]` in crate_universe when using multiple `Cargo.toml`s (#1856)
c645fe399 Silence windows build failure (#1863)
75bba7b50 Make rust_clippy providers match rustfmt_test (#1806)
f09d72755 Fix test assertion for arm64 macs (#1845)
f4113bfe1 Fix tests for new Apple toolchain (#1844)
20ce44e30 fix: use target_triple struct instead of string (#1835)
bdbded181 Fix code example in doc (#1838)
4f4014052 Fix typo: plced -> placed (#1834)
baeb0664d Remove ios/android/wasm support for gen_rust_project deps (#1684)
02557a47a Add `render_config` attribute to `crates_vendor`. (#1832)
4357fb154 Updated rules_rust to version 0.18.0 (#1829)
9adfdca9b Various cleanups (#1828)
4fa412385 Added update known shas to include T1-T2 triples (#1824)
905731ad9 Instructions on how to perform `rustfmt` check (#1822) (#1823)
108b1a187 Encapsulate running cargo into a struct (#1815)
57a099b63 Fixes resolver issue with root packages and another dependency format (#1819)
78ca9ba0a Use env method recently added to cargo_metadata (#1813)
92834930f Updated `rust_toolchain.target_json` to take encoded json strings (#1810)
84f1d0657 support `resolver = "2"` target-specific features (#1710)
a5853fd37 Use correct dynamic link args fro proc-macro crates (#1803)
b656e2553 Added tests for the `triple` constructor (#1811)
ea4a79ad9 Disable job in CI to avoid infrastructure failure. (#1816)
2fc02f030 Delete `rust_toolchain.rusrc_srcs` (#1807)
804d5fc1f Convert `rust_toolchain` attrs `exec_triple` and `target_triple` to structs (#1808)
499a2ca38 Updated platform triple values from strings to structs ("triple") (#1804)
aae1dbdcb Unify functions for computing constraint values for platform triple abi (#1805)
0d6d2b1eb Updated rules_rust version to `0.17.0` (#1800)
88e83f2df Added Rust 1.67.0 (#1799)
6922b5012 rustdoc_test: fix and test OUT_DIR (#1779)
ad01d1b0e [crate_universe] add an annotation to disable pipelining (#1733)
f651cd18f Add `CARGO_BAZEL_REPIN_ONLY` repinning allowlist (#1798)
d7f0debb0 Revert "Disable broken clang and ldd CI jobs (#1785)" (#1796)
96f82aaad Fix `cc_common.link` file output name (#1795)
5079b64d5 Fix use of `rustfmt_toolchain` when `rustc` is not provided (#1794)
23c650f35 Have `--experimental_use_cc_common_link` cover `rust_shared_library` (#1792)
ba0fb5956 Added support for `--nolegacy_external_runfiles` to `rust_doc_test` (#1790)
112242bb7 Prevent crates_vendor from restarting bazel. (#1791)
52231ef9f Added compatibility flags to `.bazelrc` to prevent regressions (#1789)
91cd399a0 Add "crate-name={}" tag to Crate Universe targets (#1787)
1b1dae196 Added Rust 1.66.1 (#1767)
fe17e8b8e Add file:// prefix to env var in docs (#1788)
0fe742bff Updated `rust_bindgen` to use `rustfmt_toolchain` (#1770)
042fd6c1c Update docs on setting Rust versions (#1786)
dddd8a0d4 Updated crate_universe dependencies (#1775)
a1330a71f Download `rustc` in `rustfmt_toolchain_repository` (#1769)
e96aad9aa Updated the ios_build example to use `crates_vendor` (#1778)
e315007df Disable broken clang and ldd CI jobs (#1785)
4e89d52a9 rustdoc_test: substitute the root of the current crate (#1777)
a52041fb5 Support `target_settings` in `rust_repository_set` and `rust_toolchain_repository` (#1758)
49906eb29 Update clippy and rustfmt aspects to require CrateInfo providers (#1772)
85564208e Updated rules_rust version to `0.16.1` (#1761)
614499a5b Fixed inability to deserialize crate_universe lockfiles (#1760)
9803d3034 Fix data and compile_data for rust_doc (#1741)
927a364cb Update Release github pipeline to trigger automatically (#1757)
7d03e05f8 Fix release pipeline (#1756)
cf7ca5dfd Updated rules_rust to version `0.16.0` (#1750)
203fe4b9a Remove unnecessary binary file (#1755)
941c7cca9 Don't propagate `compatible_with` to the underlying `cargo_build_script` `rust_binary` target (#1754)
a31490d9a Make loads from @rules_rust//rust:defs.bzl come out on one line (#1753)
7ebad4d50 Generate only the needed subset of binaries for bindgen and proto (#1751)
4ef3d4aaa Repin examples/crate_universe_unnamed (#1752)
d6e300359 Regenerate BUILD files using serde_starlark renderer (#1746)
e7c8a97d1 Convert BUILD.$name-$version.bazel to serde_starlark (#1743)
c09818d3b Exclude generated files from language stats and collapse in code review (#1747)
26a24f030 Added CI for single toolchain channel workspaces (#1712)
caed7d814 Report context on error failing to get version (#1744)
36b57af7b Add gen_binaries annotation to control which bins to make target for (#1718)
d916a6f52 crate_universe re-pinning now defaults to "workspace" (#1723)
f34661ee1 Propagate `compatible_with` attribute to the underlying `_build_script_run` target (#1745)
92977d1bf Re-pinned all dependencies managed by crate_universe (#1735)
d5289ad1c Added `rustfmt_toolchain` and refactored toolchain repository rules (#1719)
532e60ff0 Collect targets in a deterministic order (#1736)
52e02c25b Eliminate all use of hash-based collections from crate_universe (#1737)
31073ff8e Replace tera template with serde_starlark (#1734)
d4e5586d0 Support the RUNFILES_DIR environment variable. (#1732)
1357b85b1 Addressed clippy warnings from `clippy 0.1.67 (ec56537c 2022-12-15)` (#1717)
8bc9f788d Support dsym_folder output group in tests (#1703)
90c5b6eb7 Added CI for minimum supported Rust version (#1720)
be82ff8bd Match prerelease versions with annotation wildcard (#1716)
36c7f285b Arm Thumb Embedded Targets. (#1721)
5ef52e465 Update current_toolchain_files tests to use a dedicated test rule (#1714)
c75ea6f9e Add `Runfiles::current_repository` to runfiles library (#1713)
2f0511782 Updated rules_rust to version `0.15.0` (#1706)
019f87178 Added Rust 1.66.0 (#1705)
1469cd7cb Fix labels to work with canonical label literals. (#1700)
5826a500a Add riscv32imc and riscv64gc to the known sha targets (#1698)
40dee95ce Fixed typos: normla -> normal (#1699)
8f08e77ac load_arbitrary_tool uses tool_suburl to look up sha256 (#1695)
8faec3060 Fix typos in crate_universe rendered comments (#1691)
bd64711ff Silence flaky test (#1693)
46b7ea5af Added a build setting for toolchain channels (#1671)
70b272aad Updated rules_rust to version `0.14.0` (#1669)
91e597dd1 Updated all crates_vendor outputs (#1687)
9a047b0b9 Updated crate_universe dependencies (#1686)
3a91d2f5b Add RV64GC target (#1683)
d9e752ab4 Add per-toolchain `rustc_flags` (#1635)
56237415e stardoc: Use backtick not `<code>` for attr default values  (#1682)
d4b31a494 Allow passing a bazel path to vendor explicitly (#1661)
d51bf9ce0 Updated crate_universe to work with `--nolegacy_external_runfiles` (#1680)
7f40636d1 crate_universe/private/crates_vendor.bzl typo fix (#1678)
025bf7db8 Merge cc toolchain flags into build script env (#1675)
b7c36c051 Fix confusing/misleading crate_universe docs (#1677)
29233e354 Revert #1564 (#1663)
ed32b6de2 Common glob excludes (#1673)
61b99cdd1 fix: add space to crate data exclude list (#1665)
8bb25b8b7 Support Windows ARM64 (aarch64-pc-windows-msvc) (#1664)
ddf2a4c23 Re-render crate BUILD files after #1647 (#1655)
44c7e1588 Group deps and aliases by platform triple rather than by cfg string when generating BUILD files. This avoid bazel errors due to duplicate keys/deps. (#1647)
de18d8bb6 Allow `buildifier` attribute to be a file (#1660)
aa0815dc9 Fix naming of ambiguous libs (#1625)
ff314d4ab Also pass -c opt to tests in opt mode CI (#1626)
ff4e90515 Reenable windows job (#1658)
c45b8e91f Updated rules_rust to version `0.13.0` (#1644)
87d6b6c37 Update `//util/label` to support `+` in packages (#1654)
ab6959db5 fix: Fix issue with wasi-0.11.0+wasi-snapshot-preview1 (#1632)
28c090ed0 Replaced custom platform constraint values with aliases to `@platforms` (#1652)
dfbea4f52 Deprecated `rust_toolchain.rustc_srcs` (#1653)
fd1db4391 Remove deprecated attributes from rust_toolchain and cargo_bootstrap (#1651)
c8ab970c4 Generated rust-project.json files now include sysroot paths (#1641)
0a3e04cf9 Fix vendoring when not in a package (#1646)
aece1e37d Deduplicate expand_location targets in rust-project.json crate creation to avoid a bazel crash (#1639)
03a0b2483 [docs] Fixing typos in CargoConfig doc strings (#1638)
bd4fd2ac5 Upgraded cfg-expr dependency to 0.12.0. (#1636)
330554a13 Disable failing job in CI (#1640)
849f106e6 Consider compilation mode when choosing `pic`/`nopic` object files to link (#1624)
53491d719 Updated rules_rust to version `0.12.0` (#1630)
8e8843724 Remove empty glob (#1628)
1f621a944 Added Rust 1.65.0 (#1627)
c6af4d025 Add `-c opt` mode to CI (#1621)
95320cc8b process_wrapper: print line on error to parse message as json (#1566)
81eaccf39 Fixed CI breakage (#1619)
478fc3a57 Fix ambiguous native dependencies in `proc_macro`s and `staticlib`s (#1611)
9e3d8415e Build deps of _build_script_run in 'exec' mode (#1561)
ea031082b Fixed outdated docs (#1614)
a8c540e49 Restore support for old cargo_build_script load statements (#1613)
295b5ccc7 Renamed `_build_script_run` rule to `cargo_build_script` (#1612)
3778069ec Remove references to Google mirror in docs (#1607)
aad54ba29 Updated crate_universe dependencies (#1606)
c349df2a6 Remove Google mirror from Starlark snippet in release notes (#1604)
0493b998d Avoid rendering a mock root package when possible (#1596)
b04aa053c process_wrapper: Apply substitutions to param files (#1565)
b209b3e15 Updated rules_rust to version `0.11.0`. (#1587)
b1079453b Typo correction on doc (#1593)
ca5678266 Updated crate_universe dependencies (#1591)
a364d448f Fixes crates_vendor labels in remote mode when used from the root workspace (#1575)
1cc37c268 Expose the output directory from cargo_build_script (#1588)
7ffe0a555 Ignore non-utf8 text in build script output (#1583)
c5b38fe00 Merge runfiles from transitive dependencies of rust_test crate attr (#1487)
da3d522d5 Fix build scripts targeting the wrong architecture (#1564)
d288ed634 Add `out_dir` support in `cargo_dep_env` (#1571)
78d6c1b46 fix: incorrect rustfmt edition query (#1582)
48927127e Set CARGO_MANIFEST_DIR at runtime for tests (#1559)
76bd69033 Add an output group for the .rmeta (#1585)
352bfeb05 Cleanup deprecated code (#1577)
86dc561f9 Move crate_root_src to utils (#1570)
beb554eb9 update to wasm-bindgen v0.2.83 (#1567)
73fd1616b Export AbsoluteLabel functionality (#1568)
c57e7a399 Remap $PWD to empty string instead of "." (#1563)
f0cdcedc2 Added Rust 1.64.0 (#1562)
1d326554a Update docs to show release policies and support (#1560)
78c793a0a Fix markdown typo in rust_analyzer.md (#1553)
c13980fb6 Add iOS examples (#1546)
8a5e07e9f Update apple_support (#1549)
6dacd9803 Strip leading '@'s for labels in the splicing manifest (#1547)
f73d1d6fb use crate_info.deps in establish_cc_info (#1543)
4845af6c0 Add android example (#1545)
9570b7aa7 Remove -lgcc from Android builds (#1541)
cb9ca1b81 Fix crate_universe/private/srcs.bzl to work with repo mappings (#1540)
d1fc9accc Minor cleanup of CI pipelines (#1534)
2bb077b3b Updated rules_rust to version 0.10.0 (#1533)
b8751b860 add cc config info to dummy wasm32 cc toolchain (#1532)
f5ed797ee Updates rust_test to use main.rs as the root when use_libtest_harness is false (#1518)
cfcaf21d5 Preserve directory structure of source files when some are generated (#1526)
51c065841 migrating to rbe_preconfig and remove bazel_toolchains (#1524)
055abd402 Fix typo in an example of crates_repository rule (#1520)
8bfed1cd2 Added Rust 1.63.0 (#1512)
3a69ce09b Update wasm_bindgen to 0.2.82 (#1513)

git-subtree-dir: third_party/rules_rust
git-subtree-split: bf9ddeb7c83a9fe8a7d87c76134cdd8e16131b62
Signed-off-by: Adam Snaider <adsnaider@gmail.com>
Change-Id: Id9490c68d6221da66953a915a25042ef8b848505
diff --git a/crate_universe/src/utils/starlark/glob.rs b/crate_universe/src/utils/starlark/glob.rs
index 23b17a5..a7bcebb 100644
--- a/crate_universe/src/utils/starlark/glob.rs
+++ b/crate_universe/src/utils/starlark/glob.rs
@@ -1,16 +1,93 @@
-use serde::{Deserialize, Serialize};
+use std::collections::BTreeSet;
+use std::fmt;
 
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Clone)]
+use serde::de::value::{MapAccessDeserializer, SeqAccessDeserializer};
+use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
+use serde::ser::{Serialize, SerializeStruct, Serializer};
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone)]
 pub struct Glob {
-    pub include: Vec<String>,
-    pub exclude: Vec<String>,
+    pub include: BTreeSet<String>,
+    pub exclude: BTreeSet<String>,
 }
 
 impl Glob {
     pub fn new_rust_srcs() -> Self {
         Self {
-            include: vec!["**/*.rs".to_owned()],
-            ..Default::default()
+            include: BTreeSet::from(["**/*.rs".to_owned()]),
+            exclude: BTreeSet::new(),
         }
     }
+
+    pub fn is_empty(&self) -> bool {
+        self.include.is_empty()
+        // Note: self.exclude intentionally not considered. A glob is empty if
+        // there are no included globs. A glob cannot have only excludes.
+    }
+}
+
+impl Serialize for Glob {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        if self.exclude.is_empty() {
+            // Serialize as glob([...]).
+            serializer.serialize_newtype_struct("glob", &self.include)
+        } else {
+            // Serialize as glob(include = [...], exclude = [...]).
+            let mut call = serializer.serialize_struct("glob", 2)?;
+            call.serialize_field("include", &self.include)?;
+            call.serialize_field("exclude", &self.exclude)?;
+            call.end()
+        }
+    }
+}
+
+struct GlobVisitor;
+
+impl<'de> Deserialize<'de> for Glob {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        deserializer.deserialize_any(GlobVisitor)
+    }
+}
+
+impl<'de> Visitor<'de> for GlobVisitor {
+    type Value = Glob;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str("glob")
+    }
+
+    // Deserialize ["included","globs","only"]
+    fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
+    where
+        A: SeqAccess<'de>,
+    {
+        Ok(Glob {
+            include: BTreeSet::deserialize(SeqAccessDeserializer::new(seq))?,
+            exclude: BTreeSet::new(),
+        })
+    }
+
+    // Deserialize {"include":["included","globs"],"exclude":["excluded","globs"]}
+    fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
+    where
+        A: MapAccess<'de>,
+    {
+        #[derive(serde::Deserialize)]
+        struct GlobMap {
+            include: BTreeSet<String>,
+            exclude: BTreeSet<String>,
+        }
+
+        let glob_map = GlobMap::deserialize(MapAccessDeserializer::new(map))?;
+        Ok(Glob {
+            include: glob_map.include,
+            exclude: glob_map.exclude,
+        })
+    }
 }
diff --git a/crate_universe/src/utils/starlark/label.rs b/crate_universe/src/utils/starlark/label.rs
index a21c81e..c074a62 100644
--- a/crate_universe/src/utils/starlark/label.rs
+++ b/crate_universe/src/utils/starlark/label.rs
@@ -20,16 +20,17 @@
     type Err = anyhow::Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let re = Regex::new(r"^(@[\w\d\-_\.]*)?/{0,2}([\w\d\-_\./]+)?:?([\+\w\d\-_\./]+)$")?;
+        let re = Regex::new(r"^(@@?[\w\d\-_\.]*)?/{0,2}([\w\d\-_\./+]+)?(:([\+\w\d\-_\./]+))?$")?;
         let cap = re
             .captures(s)
-            .with_context(|| format!("Failed to parse label from string: {}", s))?;
+            .with_context(|| format!("Failed to parse label from string: {s}"))?;
 
         let repository = cap
             .get(1)
             .map(|m| m.as_str().trim_start_matches('@').to_owned());
+
         let package = cap.get(2).map(|m| m.as_str().to_owned());
-        let mut target = cap.get(3).map(|m| m.as_str().to_owned());
+        let mut target = cap.get(4).map(|m| m.as_str().to_owned());
 
         if target.is_none() {
             if let Some(pkg) = &package {
@@ -56,14 +57,14 @@
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         // Add the repository
         if let Some(repo) = &self.repository {
-            write!(f, "@{}", repo)?;
+            write!(f, "@{repo}")?;
         }
 
         write!(f, "//")?;
 
         // Add the package
         if let Some(pkg) = &self.package {
-            write!(f, "{}", pkg)?;
+            write!(f, "{pkg}")?;
         }
 
         write!(f, ":{}", self.target)?;
@@ -192,6 +193,15 @@
     use tempfile::tempdir;
 
     #[test]
+    fn full_label_bzlmod() {
+        let label = Label::from_str("@@repo//package/sub_package:target").unwrap();
+        assert_eq!(label.to_string(), "@repo//package/sub_package:target");
+        assert_eq!(label.repository.unwrap(), "repo");
+        assert_eq!(label.package.unwrap(), "package/sub_package");
+        assert_eq!(label.target, "target");
+    }
+
+    #[test]
     fn full_label() {
         let label = Label::from_str("@repo//package/sub_package:target").unwrap();
         assert_eq!(label.to_string(), "@repo//package/sub_package:target");
@@ -258,13 +268,25 @@
     }
 
     #[test]
+    fn label_contains_plus() {
+        let label = Label::from_str("@repo//vendor/wasi-0.11.0+wasi-snapshot-preview1:BUILD.bazel")
+            .unwrap();
+        assert_eq!(label.repository.unwrap(), "repo");
+        assert_eq!(
+            label.package.unwrap(),
+            "vendor/wasi-0.11.0+wasi-snapshot-preview1"
+        );
+        assert_eq!(label.target, "BUILD.bazel");
+    }
+
+    #[test]
     fn invalid_double_colon() {
         assert!(Label::from_str("::target").is_err());
     }
 
     #[test]
-    fn invalid_double_at() {
-        assert!(Label::from_str("@@repo//pkg:target").is_err());
+    fn invalid_triple_at() {
+        assert!(Label::from_str("@@@repo//pkg:target").is_err());
     }
 
     #[test]
@@ -282,8 +304,8 @@
         let actual_file = subdir.join("greatgrandchild");
         create_dir_all(subdir).unwrap();
         {
-            File::create(&workspace).unwrap();
-            File::create(&build_file).unwrap();
+            File::create(workspace).unwrap();
+            File::create(build_file).unwrap();
             File::create(&actual_file).unwrap();
         }
         let label = Label::from_absolute_path(&actual_file).unwrap();
@@ -300,7 +322,7 @@
         let actual_file = subdir.join("greatgrandchild");
         create_dir_all(subdir).unwrap();
         {
-            File::create(&build_file).unwrap();
+            File::create(build_file).unwrap();
             File::create(&actual_file).unwrap();
         }
         let err = Label::from_absolute_path(&actual_file)
@@ -318,7 +340,7 @@
         let actual_file = subdir.join("greatgrandchild");
         create_dir_all(subdir).unwrap();
         {
-            File::create(&workspace).unwrap();
+            File::create(workspace).unwrap();
             File::create(&actual_file).unwrap();
         }
         let err = Label::from_absolute_path(&actual_file)
diff --git a/crate_universe/src/utils/starlark/select.rs b/crate_universe/src/utils/starlark/select.rs
index 4a8a3cc..f01acb0 100644
--- a/crate_universe/src/utils/starlark/select.rs
+++ b/crate_universe/src/utils/starlark/select.rs
@@ -1,6 +1,11 @@
-use serde::{Deserialize, Serialize};
 use std::collections::{btree_set, BTreeMap, BTreeSet};
-use std::iter::once;
+use std::iter::{once, FromIterator};
+
+use serde::ser::{SerializeMap, SerializeTupleStruct, Serializer};
+use serde::{Deserialize, Serialize};
+use serde_starlark::{FunctionCall, LineComment, MULTILINE};
+
+use crate::utils::starlark::serialize::MultilineArray;
 
 pub trait SelectMap<T, U> {
     // A selectable should also implement a `map` function allowing one type of selectable
@@ -19,8 +24,16 @@
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Clone)]
 pub struct SelectList<T: Ord> {
+    // Invariant: any T in `common` is not anywhere in `selects`.
     common: BTreeSet<T>,
+    // Invariant: none of the sets are empty.
     selects: BTreeMap<String, BTreeSet<T>>,
+    // Elements that used to be in `selects` before the most recent
+    // `remap_configurations` operation, but whose old configuration did not get
+    // mapped to any new configuration. They could be ignored, but are preserved
+    // here to generate comments that help the user understand what happened.
+    #[serde(skip_serializing_if = "BTreeSet::is_empty", default = "BTreeSet::new")]
+    unmapped: BTreeSet<T>,
 }
 
 impl<T: Ord> Default for SelectList<T> {
@@ -28,6 +41,7 @@
         Self {
             common: BTreeSet::new(),
             selects: BTreeMap::new(),
+            unmapped: BTreeSet::new(),
         }
     }
 }
@@ -37,25 +51,22 @@
     pub fn insert(&mut self, value: T, configuration: Option<String>) {
         match configuration {
             None => {
+                self.selects.retain(|_, set| {
+                    set.remove(&value);
+                    !set.is_empty()
+                });
                 self.common.insert(value);
             }
             Some(cfg) => {
-                match self.selects.get_mut(&cfg) {
-                    None => {
-                        let mut set = BTreeSet::new();
-                        set.insert(value);
-                        self.selects.insert(cfg, set);
-                    }
-                    Some(set) => {
-                        set.insert(value);
-                    }
-                };
+                if !self.common.contains(&value) {
+                    self.selects.entry(cfg).or_default().insert(value);
+                }
             }
-        };
+        }
     }
 
     // TODO: This should probably be added to the [Select] trait
-    pub fn get_iter<'a>(&'a self, config: Option<&String>) -> Option<btree_set::Iter<T>> {
+    pub fn get_iter(&self, config: Option<&String>) -> Option<btree_set::Iter<T>> {
         match config {
             Some(conf) => self.selects.get(conf).map(|set| set.iter()),
             None => Some(self.common.iter()),
@@ -63,8 +74,208 @@
     }
 
     /// Determine whether or not the select should be serialized
-    pub fn should_skip_serializing(&self) -> bool {
-        self.common.is_empty() && self.selects.is_empty()
+    pub fn is_empty(&self) -> bool {
+        self.common.is_empty() && self.selects.is_empty() && self.unmapped.is_empty()
+    }
+
+    /// Maps configuration names by `f`. This function must be injective
+    /// (that is `a != b --> f(a) != f(b)`).
+    pub fn map_configuration_names<F>(self, mut f: F) -> Self
+    where
+        F: FnMut(String) -> String,
+    {
+        Self {
+            common: self.common,
+            selects: self.selects.into_iter().map(|(k, v)| (f(k), v)).collect(),
+            unmapped: self.unmapped,
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
+pub struct WithOriginalConfigurations<T> {
+    value: T,
+    original_configurations: Option<BTreeSet<String>>,
+}
+
+impl<T: Ord + Clone> SelectList<T> {
+    /// Generates a new SelectList re-keyed by the given configuration mapping.
+    /// This mapping maps from configurations in the current SelectList to sets of
+    /// configurations in the new SelectList.
+    pub fn remap_configurations(
+        self,
+        mapping: &BTreeMap<String, BTreeSet<String>>,
+    ) -> SelectList<WithOriginalConfigurations<T>> {
+        // Map new configuration -> value -> old configurations.
+        let mut remapped: BTreeMap<String, BTreeMap<T, BTreeSet<String>>> = BTreeMap::new();
+        // Map value -> old configurations.
+        let mut unmapped: BTreeMap<T, BTreeSet<String>> = BTreeMap::new();
+
+        for (original_configuration, values) in self.selects {
+            match mapping.get(&original_configuration) {
+                Some(configurations) => {
+                    for configuration in configurations {
+                        for value in &values {
+                            remapped
+                                .entry(configuration.clone())
+                                .or_default()
+                                .entry(value.clone())
+                                .or_default()
+                                .insert(original_configuration.clone());
+                        }
+                    }
+                }
+                None => {
+                    for value in values {
+                        unmapped
+                            .entry(value)
+                            .or_default()
+                            .insert(original_configuration.clone());
+                    }
+                }
+            }
+        }
+
+        SelectList {
+            common: self
+                .common
+                .into_iter()
+                .map(|value| WithOriginalConfigurations {
+                    value,
+                    original_configurations: None,
+                })
+                .collect(),
+            selects: remapped
+                .into_iter()
+                .map(|(new_configuration, value_to_original_configuration)| {
+                    (
+                        new_configuration,
+                        value_to_original_configuration
+                            .into_iter()
+                            .map(
+                                |(value, original_configurations)| WithOriginalConfigurations {
+                                    value,
+                                    original_configurations: Some(original_configurations),
+                                },
+                            )
+                            .collect(),
+                    )
+                })
+                .collect(),
+            unmapped: unmapped
+                .into_iter()
+                .map(
+                    |(value, original_configurations)| WithOriginalConfigurations {
+                        value,
+                        original_configurations: Some(original_configurations),
+                    },
+                )
+                .collect(),
+        }
+    }
+}
+
+#[derive(Serialize)]
+#[serde(rename = "selects.NO_MATCHING_PLATFORM_TRIPLES")]
+struct NoMatchingPlatformTriples;
+
+// TODO: after removing the remaining tera template usages of SelectList, this
+// inherent method should become the Serialize impl.
+impl<T: Ord> SelectList<T> {
+    pub fn serialize_starlark<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        T: Serialize,
+        S: Serializer,
+    {
+        // Output looks like:
+        //
+        //     [
+        //         "common...",
+        //     ] + select({
+        //         "configuration": [
+        //             "value...",  # cfg(whatever)
+        //         ],
+        //         "//conditions:default": [],
+        //     })
+        //
+        // The common part and select are each omitted if they are empty (except
+        // if the entire thing is empty, in which case we serialize the common
+        // part to get an empty array).
+        //
+        // If there are unmapped entries, we include them like this:
+        //
+        //     [
+        //         "common...",
+        //     ] + selects.with_unmapped({
+        //         "configuration": [
+        //             "value...",  # cfg(whatever)
+        //         ],
+        //         "//conditions:default": [],
+        //         selects.NO_MATCHING_PLATFORM_TRIPLES: [
+        //             "value...",  # cfg(obscure)
+        //         ],
+        //     })
+
+        let mut plus = serializer.serialize_tuple_struct("+", MULTILINE)?;
+
+        if !self.common.is_empty() || self.selects.is_empty() && self.unmapped.is_empty() {
+            plus.serialize_field(&MultilineArray(&self.common))?;
+        }
+
+        if !self.selects.is_empty() || !self.unmapped.is_empty() {
+            struct SelectInner<'a, T: Ord>(&'a SelectList<T>);
+
+            impl<'a, T> Serialize for SelectInner<'a, T>
+            where
+                T: Ord + Serialize,
+            {
+                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+                where
+                    S: Serializer,
+                {
+                    let mut map = serializer.serialize_map(Some(MULTILINE))?;
+                    for (cfg, value) in &self.0.selects {
+                        map.serialize_entry(cfg, &MultilineArray(value))?;
+                    }
+                    map.serialize_entry("//conditions:default", &[] as &[T])?;
+                    if !self.0.unmapped.is_empty() {
+                        map.serialize_entry(
+                            &NoMatchingPlatformTriples,
+                            &MultilineArray(&self.0.unmapped),
+                        )?;
+                    }
+                    map.end()
+                }
+            }
+
+            let function = if self.unmapped.is_empty() {
+                "select"
+            } else {
+                "selects.with_unmapped"
+            };
+
+            plus.serialize_field(&FunctionCall::new(function, [SelectInner(self)]))?;
+        }
+
+        plus.end()
+    }
+}
+
+impl<T> Serialize for WithOriginalConfigurations<T>
+where
+    T: Serialize,
+{
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        if let Some(original_configurations) = &self.original_configurations {
+            let comment =
+                Vec::from_iter(original_configurations.iter().map(String::as_str)).join(", ");
+            LineComment::new(&self.value, &comment).serialize(serializer)
+        } else {
+            self.value.serialize(serializer)
+        }
     }
 }
 
@@ -82,21 +293,43 @@
     type Mapped = SelectList<U>;
 
     fn map<F: Copy + Fn(T) -> U>(self, func: F) -> Self::Mapped {
+        let common: BTreeSet<U> = self.common.into_iter().map(func).collect();
+        let selects: BTreeMap<String, BTreeSet<U>> = self
+            .selects
+            .into_iter()
+            .filter_map(|(key, set)| {
+                let set: BTreeSet<U> = set
+                    .into_iter()
+                    .map(func)
+                    .filter(|value| !common.contains(value))
+                    .collect();
+                if set.is_empty() {
+                    None
+                } else {
+                    Some((key, set))
+                }
+            })
+            .collect();
         SelectList {
-            common: self.common.into_iter().map(func).collect(),
-            selects: self
-                .selects
-                .into_iter()
-                .map(|(key, map)| (key, map.into_iter().map(func).collect()))
-                .collect(),
+            common,
+            selects,
+            unmapped: self.unmapped.into_iter().map(func).collect(),
         }
     }
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Clone)]
 pub struct SelectDict<T: Ord> {
+    // Invariant: keys in this map are not in any of the inner maps of `selects`.
     common: BTreeMap<String, T>,
+    // Invariant: none of the inner maps are empty.
     selects: BTreeMap<String, BTreeMap<String, T>>,
+    // Elements that used to be in `selects` before the most recent
+    // `remap_configurations` operation, but whose old configuration did not get
+    // mapped to any new configuration. They could be ignored, but are preserved
+    // here to generate comments that help the user understand what happened.
+    #[serde(skip_serializing_if = "BTreeMap::is_empty", default = "BTreeMap::new")]
+    unmapped: BTreeMap<String, T>,
 }
 
 impl<T: Ord> Default for SelectDict<T> {
@@ -104,35 +337,200 @@
         Self {
             common: BTreeMap::new(),
             selects: BTreeMap::new(),
+            unmapped: BTreeMap::new(),
         }
     }
 }
 
 impl<T: Ord> SelectDict<T> {
-    // TODO: This should probably be added to the [Select] trait
-    pub fn insert(&mut self, value: BTreeMap<String, T>, configuration: Option<String>) {
+    pub fn insert(&mut self, key: String, value: T, configuration: Option<String>) {
         match configuration {
             None => {
-                self.common.extend(value);
+                self.selects.retain(|_, map| {
+                    map.remove(&key);
+                    !map.is_empty()
+                });
+                self.common.insert(key, value);
             }
             Some(cfg) => {
-                match self.selects.get_mut(&cfg) {
-                    None => {
-                        let mut set = BTreeMap::new();
-                        set.extend(value);
-                        self.selects.insert(cfg, set);
-                    }
-                    Some(set) => {
-                        set.extend(value);
-                    }
-                };
+                if !self.common.contains_key(&key) {
+                    self.selects.entry(cfg).or_default().insert(key, value);
+                }
             }
-        };
+        }
     }
 
-    /// Determine whether or not the select should be serialized
-    pub fn should_skip_serializing(&self) -> bool {
-        self.common.is_empty() && self.selects.is_empty()
+    pub fn extend(&mut self, entries: BTreeMap<String, T>, configuration: Option<String>) {
+        for (key, value) in entries {
+            self.insert(key, value, configuration.clone());
+        }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.common.is_empty() && self.selects.is_empty() && self.unmapped.is_empty()
+    }
+}
+
+impl<T: Ord + Clone> SelectDict<T> {
+    /// Generates a new SelectDict re-keyed by the given configuration mapping.
+    /// This mapping maps from configurations in the current SelectDict to sets
+    /// of configurations in the new SelectDict.
+    pub fn remap_configurations(
+        self,
+        mapping: &BTreeMap<String, BTreeSet<String>>,
+    ) -> SelectDict<WithOriginalConfigurations<T>> {
+        // Map new configuration -> entry -> old configurations.
+        let mut remapped: BTreeMap<String, BTreeMap<(String, T), BTreeSet<String>>> =
+            BTreeMap::new();
+        // Map entry -> old configurations.
+        let mut unmapped: BTreeMap<(String, T), BTreeSet<String>> = BTreeMap::new();
+
+        for (original_configuration, entries) in self.selects {
+            match mapping.get(&original_configuration) {
+                Some(configurations) => {
+                    for configuration in configurations {
+                        for (key, value) in &entries {
+                            remapped
+                                .entry(configuration.clone())
+                                .or_default()
+                                .entry((key.clone(), value.clone()))
+                                .or_default()
+                                .insert(original_configuration.clone());
+                        }
+                    }
+                }
+                None => {
+                    for (key, value) in entries {
+                        unmapped
+                            .entry((key, value))
+                            .or_default()
+                            .insert(original_configuration.clone());
+                    }
+                }
+            }
+        }
+
+        SelectDict {
+            common: self
+                .common
+                .into_iter()
+                .map(|(key, value)| {
+                    (
+                        key,
+                        WithOriginalConfigurations {
+                            value,
+                            original_configurations: None,
+                        },
+                    )
+                })
+                .collect(),
+            selects: remapped
+                .into_iter()
+                .map(|(new_configuration, entry_to_original_configuration)| {
+                    (
+                        new_configuration,
+                        entry_to_original_configuration
+                            .into_iter()
+                            .map(|((key, value), original_configurations)| {
+                                (
+                                    key,
+                                    WithOriginalConfigurations {
+                                        value,
+                                        original_configurations: Some(original_configurations),
+                                    },
+                                )
+                            })
+                            .collect(),
+                    )
+                })
+                .collect(),
+            unmapped: unmapped
+                .into_iter()
+                .map(|((key, value), original_configurations)| {
+                    (
+                        key,
+                        WithOriginalConfigurations {
+                            value,
+                            original_configurations: Some(original_configurations),
+                        },
+                    )
+                })
+                .collect(),
+        }
+    }
+}
+
+// TODO: after removing the remaining tera template usages of SelectDict, this
+// inherent method should become the Serialize impl.
+impl<T: Ord + Serialize> SelectDict<T> {
+    pub fn serialize_starlark<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        // If there are no platform-specific entries, we output just an ordinary
+        // dict.
+        //
+        // If there are platform-specific ones, we use the following. Ideally it
+        // could be done as `dicts.add({...}, select({...}))` but bazel_skylib's
+        // dicts.add does not support selects.
+        //
+        //     select({
+        //         "configuration": {
+        //             "common-key": "common-value",
+        //             "plat-key": "plat-value",  # cfg(whatever)
+        //         },
+        //         "//conditions:default": {},
+        //     })
+        //
+        // If there are unmapped entries, we include them like this:
+        //
+        //     selects.with_unmapped({
+        //         "configuration": {
+        //             "common-key": "common-value",
+        //             "plat-key": "plat-value",  # cfg(whatever)
+        //         },
+        //         "//conditions:default": [],
+        //         selects.NO_MATCHING_PLATFORM_TRIPLES: {
+        //             "unmapped-key": "unmapped-value",  # cfg(obscure)
+        //         },
+        //     })
+
+        if self.selects.is_empty() && self.unmapped.is_empty() {
+            return self.common.serialize(serializer);
+        }
+
+        struct SelectInner<'a, T: Ord>(&'a SelectDict<T>);
+
+        impl<'a, T> Serialize for SelectInner<'a, T>
+        where
+            T: Ord + Serialize,
+        {
+            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+            where
+                S: Serializer,
+            {
+                let mut map = serializer.serialize_map(Some(MULTILINE))?;
+                for (cfg, value) in &self.0.selects {
+                    let mut combined = BTreeMap::new();
+                    combined.extend(&self.0.common);
+                    combined.extend(value);
+                    map.serialize_entry(cfg, &combined)?;
+                }
+                map.serialize_entry("//conditions:default", &self.0.common)?;
+                if !self.0.unmapped.is_empty() {
+                    map.serialize_entry(&NoMatchingPlatformTriples, &self.0.unmapped)?;
+                }
+                map.end()
+            }
+        }
+
+        let function = if self.unmapped.is_empty() {
+            "select"
+        } else {
+            "selects.with_unmapped"
+        };
+
+        FunctionCall::new(function, [SelectInner(self)]).serialize(serializer)
     }
 }
 
@@ -146,21 +544,132 @@
     }
 }
 
-impl<T: Ord, U: Ord> SelectMap<T, U> for SelectDict<T> {
-    type Mapped = SelectDict<U>;
+#[cfg(test)]
+mod test {
+    use super::*;
 
-    fn map<F: Copy + Fn(T) -> U>(self, func: F) -> Self::Mapped {
-        SelectDict {
-            common: self
-                .common
-                .into_iter()
-                .map(|(key, val)| (key, func(val)))
-                .collect(),
-            selects: self
-                .selects
-                .into_iter()
-                .map(|(key, map)| (key, map.into_iter().map(|(k, v)| (k, func(v))).collect()))
-                .collect(),
-        }
+    use indoc::indoc;
+
+    #[test]
+    fn remap_select_list_configurations() {
+        let mut select_list = SelectList::default();
+        select_list.insert("dep-a".to_owned(), Some("cfg(macos)".to_owned()));
+        select_list.insert("dep-b".to_owned(), Some("cfg(macos)".to_owned()));
+        select_list.insert("dep-d".to_owned(), Some("cfg(macos)".to_owned()));
+        select_list.insert("dep-a".to_owned(), Some("cfg(x86_64)".to_owned()));
+        select_list.insert("dep-c".to_owned(), Some("cfg(x86_64)".to_owned()));
+        select_list.insert("dep-e".to_owned(), Some("cfg(pdp11)".to_owned()));
+        select_list.insert("dep-d".to_owned(), None);
+
+        let mapping = BTreeMap::from([
+            (
+                "cfg(macos)".to_owned(),
+                BTreeSet::from(["x86_64-macos".to_owned(), "aarch64-macos".to_owned()]),
+            ),
+            (
+                "cfg(x86_64)".to_owned(),
+                BTreeSet::from(["x86_64-linux".to_owned(), "x86_64-macos".to_owned()]),
+            ),
+        ]);
+
+        let mut expected = SelectList::default();
+        expected.insert(
+            WithOriginalConfigurations {
+                value: "dep-a".to_owned(),
+                original_configurations: Some(BTreeSet::from([
+                    "cfg(macos)".to_owned(),
+                    "cfg(x86_64)".to_owned(),
+                ])),
+            },
+            Some("x86_64-macos".to_owned()),
+        );
+        expected.insert(
+            WithOriginalConfigurations {
+                value: "dep-b".to_owned(),
+                original_configurations: Some(BTreeSet::from(["cfg(macos)".to_owned()])),
+            },
+            Some("x86_64-macos".to_owned()),
+        );
+        expected.insert(
+            WithOriginalConfigurations {
+                value: "dep-c".to_owned(),
+                original_configurations: Some(BTreeSet::from(["cfg(x86_64)".to_owned()])),
+            },
+            Some("x86_64-macos".to_owned()),
+        );
+        expected.insert(
+            WithOriginalConfigurations {
+                value: "dep-a".to_owned(),
+                original_configurations: Some(BTreeSet::from(["cfg(macos)".to_owned()])),
+            },
+            Some("aarch64-macos".to_owned()),
+        );
+        expected.insert(
+            WithOriginalConfigurations {
+                value: "dep-b".to_owned(),
+                original_configurations: Some(BTreeSet::from(["cfg(macos)".to_owned()])),
+            },
+            Some("aarch64-macos".to_owned()),
+        );
+        expected.insert(
+            WithOriginalConfigurations {
+                value: "dep-a".to_owned(),
+                original_configurations: Some(BTreeSet::from(["cfg(x86_64)".to_owned()])),
+            },
+            Some("x86_64-linux".to_owned()),
+        );
+        expected.insert(
+            WithOriginalConfigurations {
+                value: "dep-c".to_owned(),
+                original_configurations: Some(BTreeSet::from(["cfg(x86_64)".to_owned()])),
+            },
+            Some("x86_64-linux".to_owned()),
+        );
+        expected.insert(
+            WithOriginalConfigurations {
+                value: "dep-d".to_owned(),
+                original_configurations: None,
+            },
+            None,
+        );
+
+        expected.unmapped.insert(WithOriginalConfigurations {
+            value: "dep-e".to_owned(),
+            original_configurations: Some(BTreeSet::from(["cfg(pdp11)".to_owned()])),
+        });
+
+        let select_list = select_list.remap_configurations(&mapping);
+        assert_eq!(select_list, expected);
+
+        let expected_starlark = indoc! {r#"
+            [
+                "dep-d",
+            ] + selects.with_unmapped({
+                "aarch64-macos": [
+                    "dep-a",  # cfg(macos)
+                    "dep-b",  # cfg(macos)
+                ],
+                "x86_64-linux": [
+                    "dep-a",  # cfg(x86_64)
+                    "dep-c",  # cfg(x86_64)
+                ],
+                "x86_64-macos": [
+                    "dep-a",  # cfg(macos), cfg(x86_64)
+                    "dep-b",  # cfg(macos)
+                    "dep-c",  # cfg(x86_64)
+                ],
+                "//conditions:default": [],
+                selects.NO_MATCHING_PLATFORM_TRIPLES: [
+                    "dep-e",  # cfg(pdp11)
+                ],
+            })
+        "#};
+
+        assert_eq!(
+            select_list
+                .serialize_starlark(serde_starlark::Serializer)
+                .unwrap(),
+            expected_starlark,
+        );
     }
 }
diff --git a/crate_universe/src/utils/starlark/serialize.rs b/crate_universe/src/utils/starlark/serialize.rs
new file mode 100644
index 0000000..4d362c7
--- /dev/null
+++ b/crate_universe/src/utils/starlark/serialize.rs
@@ -0,0 +1,125 @@
+use serde::ser::{SerializeSeq, SerializeStruct, SerializeTupleStruct, Serializer};
+use serde::Serialize;
+use serde_starlark::{FunctionCall, MULTILINE, ONELINE};
+
+use super::{
+    Data, ExportsFiles, Load, Package, RustBinary, RustLibrary, RustProcMacro, SelectList,
+};
+
+// For structs that contain #[serde(flatten)], a quirk of how Serde processes
+// that attribute is that they get serialized as a map, not struct. In Starlark
+// unlike in JSON, maps and structs are differently serialized, so we need to
+// help fill in the function name or else we'd get a Starlark map instead.
+pub fn rust_proc_macro<S>(rule: &RustProcMacro, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+{
+    FunctionCall::new("rust_proc_macro", rule).serialize(serializer)
+}
+
+pub fn rust_library<S>(rule: &RustLibrary, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+{
+    FunctionCall::new("rust_library", rule).serialize(serializer)
+}
+
+pub fn rust_binary<S>(rule: &RustBinary, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+{
+    FunctionCall::new("rust_binary", rule).serialize(serializer)
+}
+
+// Serialize an array with each element on its own line, even if there is just a
+// single element which serde_starlark would ordinarily place on the same line
+// as the array brackets.
+pub struct MultilineArray<'a, A>(pub &'a A);
+
+impl<'a, A, T> Serialize for MultilineArray<'a, A>
+where
+    &'a A: IntoIterator<Item = &'a T>,
+    T: Serialize + 'a,
+{
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut array = serializer.serialize_seq(Some(serde_starlark::MULTILINE))?;
+        for element in self.0 {
+            array.serialize_element(element)?;
+        }
+        array.end()
+    }
+}
+
+// TODO: This can go away after SelectList's derived Serialize impl (used by
+// tera) goes away and `serialize_starlark` becomes its real Serialize impl.
+#[derive(Serialize)]
+#[serde(transparent)]
+pub struct SelectListWrapper<'a, T: Ord + Serialize>(
+    #[serde(serialize_with = "SelectList::serialize_starlark")] &'a SelectList<T>,
+);
+
+impl Serialize for Load {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let line = if self.items.len() > 1 {
+            MULTILINE
+        } else {
+            ONELINE
+        };
+        let mut call = serializer.serialize_tuple_struct("load", line)?;
+        call.serialize_field(&self.bzl)?;
+        for item in &self.items {
+            call.serialize_field(item)?;
+        }
+        call.end()
+    }
+}
+
+impl Serialize for Package {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut call = serializer.serialize_struct("package", ONELINE)?;
+        call.serialize_field("default_visibility", &self.default_visibility)?;
+        call.end()
+    }
+}
+
+impl Serialize for ExportsFiles {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut call = serializer.serialize_tuple_struct("exports_files", MULTILINE)?;
+        call.serialize_field(&FunctionCall::new("+", (&self.paths, &self.globs)))?;
+        call.end()
+    }
+}
+
+impl Data {
+    pub fn is_empty(&self) -> bool {
+        self.glob.is_empty() && self.select.is_empty()
+    }
+}
+
+impl Serialize for Data {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut plus = serializer.serialize_tuple_struct("+", MULTILINE)?;
+        if !self.glob.is_empty() {
+            plus.serialize_field(&self.glob)?;
+        }
+        if !self.select.is_empty() || self.glob.is_empty() {
+            plus.serialize_field(&SelectListWrapper(&self.select))?;
+        }
+        plus.end()
+    }
+}
diff --git a/crate_universe/src/utils/starlark/target_compatible_with.rs b/crate_universe/src/utils/starlark/target_compatible_with.rs
new file mode 100644
index 0000000..1c58392
--- /dev/null
+++ b/crate_universe/src/utils/starlark/target_compatible_with.rs
@@ -0,0 +1,92 @@
+use std::collections::BTreeSet;
+
+use serde::ser::{SerializeMap, SerializeTupleStruct, Serializer};
+use serde::{Deserialize, Serialize};
+use serde_starlark::{FunctionCall, MULTILINE};
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Clone)]
+pub struct TargetCompatibleWith {
+    target_triples: BTreeSet<String>,
+}
+
+impl TargetCompatibleWith {
+    pub fn new(target_triples: BTreeSet<String>) -> Self {
+        TargetCompatibleWith { target_triples }
+    }
+
+    pub fn serialize_starlark<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        // Output looks like:
+        //
+        //     select({
+        //         "configuration": [],
+        //         "//conditions:default": ["@platforms//:incompatible"],
+        //     })
+
+        let mut plus = serializer.serialize_tuple_struct("+", MULTILINE)?;
+
+        struct SelectInner<'a>(&'a BTreeSet<String>);
+
+        impl<'a> Serialize for SelectInner<'a> {
+            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+            where
+                S: Serializer,
+            {
+                let mut map = serializer.serialize_map(Some(MULTILINE))?;
+                for cfg in self.0 {
+                    map.serialize_entry(cfg, &[] as &[String])?;
+                }
+                map.serialize_entry(
+                    "//conditions:default",
+                    &["@platforms//:incompatible".to_owned()] as &[String],
+                )?;
+                map.end()
+            }
+        }
+
+        plus.serialize_field(&FunctionCall::new(
+            "select",
+            [SelectInner(&self.target_triples)],
+        ))?;
+
+        plus.end()
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    use indoc::indoc;
+
+    #[test]
+    fn target_compatible_with() {
+        let target_compatible_with = TargetCompatibleWith::new(BTreeSet::from([
+            "@rules_rust//rust/platform:wasm32-unknown-unknown".to_owned(),
+            "@rules_rust//rust/platform:wasm32-wasi".to_owned(),
+            "@rules_rust//rust/platform:x86_64-apple-darwin".to_owned(),
+            "@rules_rust//rust/platform:x86_64-pc-windows-msvc".to_owned(),
+            "@rules_rust//rust/platform:x86_64-unknown-linux-gnu".to_owned(),
+        ]));
+
+        let expected_starlark = indoc! {r#"
+            select({
+                "@rules_rust//rust/platform:wasm32-unknown-unknown": [],
+                "@rules_rust//rust/platform:wasm32-wasi": [],
+                "@rules_rust//rust/platform:x86_64-apple-darwin": [],
+                "@rules_rust//rust/platform:x86_64-pc-windows-msvc": [],
+                "@rules_rust//rust/platform:x86_64-unknown-linux-gnu": [],
+                "//conditions:default": ["@platforms//:incompatible"],
+            })
+        "#};
+
+        assert_eq!(
+            target_compatible_with
+                .serialize_starlark(serde_starlark::Serializer)
+                .unwrap(),
+            expected_starlark,
+        );
+    }
+}