Skip to content

Commit

Permalink
[antlir2][feature] make features .so plugins
Browse files Browse the repository at this point in the history
Summary:
Compile features as `.so` plugins instead of full-on `rust_binary` targets.

This makes a lot of things nicer, safer and easier to do, but primarily this
makes it so that we get `tracing` to "just work (TM)".

NOTE: If we actually want to implement features in some other language, we can
still have a rust version that just proxies to a separate binary as the
previous setup did.

Test Plan:
```
❯ buck2 test fbcode//antlir/antlir2/test_images/...
Buck UI: https://www.internalfb.com/buck2/f5c9cff6-74aa-463f-b4e2-9c3986a171df
Test UI: https://www.internalfb.com/intern/testinfra/testrun/2533274988662716
Network: Up: 1.2MiB  Down: 288B  (reSessionID-738635b3-71a3-4a9b-a443-c81076a006ae)
Jobs completed: 184. Time elapsed: 1:57.2s.
Cache hits: 0%. Commands: 21 (cached: 0, remote: 0, local: 21)
Tests finished: Pass 72. Fail 0. Fatal 0. Skip 0. Build failure 0
```

Logs work
```
❯ buck2 build --show-output fbcode//antlir/antlir2/test_images/install:install-file[debug][compile][logs]
Buck UI: https://www.internalfb.com/buck2/da8c2a37-114b-41f4-82c2-4ad98f8476cb
Network: Up: 30MiB  Down: 960B  (reSessionID-cb3ea9f0-0576-4ee1-96ac-8f1857c6e928)
Jobs completed: 75886. Time elapsed: 24.7s.
Cache hits: 0%. Commands: 16 (cached: 0, remote: 0, local: 16)
BUILD SUCCEEDED
fbcode//antlir/antlir2/test_images/install:install-file[debug][compile][logs] buck-out/v2/gen/fbcode/fdd3fedc5f835a2f/antlir/antlir2/test_images/install/__install-file__/compile_logs

[email protected] in fbsource
❯ tail buck-out/v2/gen/fbcode/fdd3fedc5f835a2f/antlir/antlir2/test_images/install/__install-file__/compile_logs/compile.log
...

I1002 17:30:18.376307     2 fbcode/antlir/antlir2/features/install.rs:278] [compile, install{self: Install { dst: "/hello", group: "root", mode: Mode(438), src: "fbcode/antlir/antlir2/test_images/install/hello.txt", user: "root", binary_info: None }}] return: ()
...

```

Reviewed By: sergeyfd

Differential Revision: D48046541

fbshipit-source-id: 46e41d984092f3c34040d000e5bf7b8a0eb5a9c5
  • Loading branch information
vmagro authored and facebook-github-bot committed Oct 2, 2023
1 parent a5e3a58 commit fd4d56a
Show file tree
Hide file tree
Showing 32 changed files with 374 additions and 303 deletions.
2 changes: 0 additions & 2 deletions antlir/antlir2/antlir2/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ impl LogArgs {
}
(None, Some(append_path)) => Ok(Some(
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(append_path)
.context("while opening logs file for appending")?,
Expand Down Expand Up @@ -133,7 +132,6 @@ fn main() -> Result<()> {
.fmt_fields(tracing_glog::GlogFields::default())
.with_writer(file)
}))
.with(tracing_subscriber::EnvFilter::from_default_env())
.init();

let rootless = antlir2_rootless::init().context("while setting up antlir2_rootless")?;
Expand Down
3 changes: 1 addition & 2 deletions antlir/antlir2/antlir2_compile/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ rust_library(
]),
deps = [
"anyhow",
"memfd",
"libloading",
"serde",
"serde_json",
"thiserror",
"tracing",
"xattr",
Expand Down
87 changes: 34 additions & 53 deletions antlir/antlir2/antlir2_compile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,11 @@
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fmt::Display;
use std::fs::File;
use std::io::Seek;
use std::io::Write;
use std::os::fd::IntoRawFd;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;

use antlir2_features::Feature;
use anyhow::anyhow;
use anyhow::Context;
use buck_label::Label;
use serde::Deserialize;
use serde::Serialize;
Expand Down Expand Up @@ -52,6 +45,8 @@ pub enum Error {
#[error("install dst {dst:?} is claiming to be a directory, but {src:?} is a file")]
InstallDstIsDirectoryButNotSrc { src: PathBuf, dst: PathBuf },
#[error(transparent)]
Feature(#[from] antlir2_features::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}

Expand Down Expand Up @@ -246,8 +241,6 @@ impl CompilerContext {
}

pub trait CompileFeature {
fn base_compileish_cmd(&self, sub: &'static str, ctx: &CompilerContext) -> Result<Command>;

fn compile(&self, ctx: &CompilerContext) -> Result<()>;

/// Add details about this [Feature] to the compiler [plan::Plan].
Expand All @@ -256,57 +249,45 @@ pub trait CompileFeature {
}
}

fn ctx_memfd(ctx: &CompilerContext) -> Result<File> {
let opts = memfd::MemfdOptions::default().close_on_exec(false);
let mfd = opts.create("ctx").context("while creating memfd")?;
serde_json::to_writer(&mut mfd.as_file(), &ctx).context("while serializing CompilerContext")?;
mfd.as_file().rewind()?;
Ok(mfd.into_file())
/// PluginExt indirects the implementation of [CompileFeature] through a .so
/// plugin. The underlying crates all provide a type that implements
/// [CompileFeature], and some generated code provides a set of exported symbols
/// that let us call that implementation.
trait PluginExt {
fn compile_fn(
&self,
) -> Result<libloading::Symbol<fn(&antlir2_features::Feature, &CompilerContext) -> Result<()>>>;
fn plan_fn(
&self,
) -> Result<
libloading::Symbol<
fn(&antlir2_features::Feature, &CompilerContext) -> Result<Vec<plan::Item>>,
>,
>;
}

impl CompileFeature for Feature {
fn base_compileish_cmd(&self, sub: &'static str, ctx: &CompilerContext) -> Result<Command> {
let ctx_file = ctx_memfd(ctx).context("while serializing CompilerContext")?;
let mut cmd = Feature::base_cmd(self);
cmd.arg(sub)
.arg("--ctx")
.arg(Path::new("/proc/self/fd").join(ctx_file.into_raw_fd().to_string()))
.env("RUST_LOG", "trace");
Ok(cmd)
impl PluginExt for antlir2_features::Plugin {
fn compile_fn(
&self,
) -> Result<libloading::Symbol<fn(&Feature, &CompilerContext) -> Result<()>>> {
self.get_symbol(b"CompileFeature_compile\0")
.map_err(|e| anyhow::anyhow!("while getting compile function: {e}").into())
}

fn plan_fn(
&self,
) -> Result<libloading::Symbol<fn(&Feature, &CompilerContext) -> Result<Vec<plan::Item>>>> {
self.get_symbol(b"CompileFeature_plan\0")
.map_err(|e| anyhow::anyhow!("while getting plan function: {e}").into())
}
}

impl CompileFeature for Feature {
fn compile(&self, ctx: &CompilerContext) -> Result<()> {
let res = self
.base_compileish_cmd("compile", ctx)?
.output()
.context("while running feature cmd")?;
if res.status.success() {
Ok(())
} else {
std::io::stdout().write_all(&res.stdout)?;
std::io::stderr().write_all(&res.stderr)?;
Err(anyhow!("feature cmd failed").into())
}
self.plugin()?.compile_fn()?(self, ctx)
}

fn plan(&self, ctx: &CompilerContext) -> Result<Vec<plan::Item>> {
let res = self
.base_compileish_cmd("plan", ctx)?
.output()
.context("while running feature cmd")?;
tracing::trace!(
"got plan: {}",
std::str::from_utf8(&res.stdout).unwrap_or("not utf8")
);
if res.status.success() {
Ok(serde_json::from_slice(&res.stdout).context("while parsing feature plan")?)
} else {
Err(anyhow!(
"feature cmd failed:\nstdout: {}\nstderr: {}",
std::str::from_utf8(&res.stdout).unwrap_or(&String::from_utf8_lossy(&res.stdout)),
std::str::from_utf8(&res.stderr).unwrap_or(&String::from_utf8_lossy(&res.stderr)),
)
.into())
}
self.plugin()?.plan_fn()?(self, ctx)
}
}
2 changes: 1 addition & 1 deletion antlir/antlir2/antlir2_depgraph/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ rust_library(
deps = [
"derivative",
"itertools",
"libloading",
"nix",
"petgraph",
"serde",
"serde_json",
"serde_with",
"thiserror",
"tracing",
Expand Down
2 changes: 1 addition & 1 deletion antlir/antlir2/antlir2_depgraph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub mod item;
use item::Item;
use item::ItemKey;
pub mod requires_provides;
use requires_provides::FeatureExt as _;
use requires_provides::RequiresProvides as _;
use requires_provides::Validator;
mod node;
use node::GraphExt;
Expand Down
79 changes: 40 additions & 39 deletions antlir/antlir2/antlir2_depgraph/src/requires_provides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
*/

use std::fmt::Debug;
use std::io::Cursor;

use antlir2_features::Feature;
use nix::sys::stat::Mode;
use serde::Deserialize;
use serde::Serialize;
use tracing::trace;

use crate::item::FileType;
use crate::item::Item;
Expand Down Expand Up @@ -122,48 +120,51 @@ impl<'a> Validator<'a> {
}
}

pub(crate) trait FeatureExt<'f> {
fn provides(&self) -> std::result::Result<Vec<Item<'f>>, String>;
fn requires(&self) -> std::result::Result<Vec<Requirement<'f>>, String>;
pub trait RequiresProvides {
fn provides(&self) -> std::result::Result<Vec<Item<'static>>, String>;
fn requires(&self) -> std::result::Result<Vec<Requirement<'static>>, String>;
}

impl<'f> FeatureExt<'f> for Feature {
/// PluginExt indirects the implementation of [RequiresProvides] through a .so
/// plugin. The underlying crates all provide a type that implements
/// [RequiresProvides], and some generated code provides a set of exported
/// symbols that let us call that implementation.
trait PluginExt {
fn provides_fn(
&self,
) -> Result<libloading::Symbol<fn(&Feature) -> Result<Vec<Item<'static>>, String>>, String>;

fn requires_fn(
&self,
) -> Result<libloading::Symbol<fn(&Feature) -> Result<Vec<Requirement<'static>>, String>>, String>;
}

impl PluginExt for antlir2_features::Plugin {
fn provides_fn(
&self,
) -> Result<libloading::Symbol<fn(&Feature) -> Result<Vec<Item<'static>>, String>>, String>
{
self.get_symbol(b"RequiresProvides_provides\0")
.map_err(|e| format!("failed to get provides fn: {e}"))
}

fn requires_fn(
&self,
) -> Result<libloading::Symbol<fn(&Feature) -> Result<Vec<Requirement<'static>>, String>>, String>
{
self.get_symbol(b"RequiresProvides_requires\0")
.map_err(|e| format!("failed to get provides fn: {e}"))
}
}

impl RequiresProvides for Feature {
#[tracing::instrument]
fn provides(&self) -> std::result::Result<Vec<Item<'f>>, String> {
let mut cmd = self.base_cmd();
cmd.arg("provides");
trace!("running {cmd:?} to determine provides");
let out = cmd
.output()
.map_err(|e| format!("failed to run cmd {cmd:?}: {e}"))?;
if !out.status.success() {
Err(format!(
"{cmd:?} failed with exit code {}\n{}\n{}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
))
} else {
let mut deser = serde_json::Deserializer::from_reader(Cursor::new(&out.stdout));
<Vec<Item<'f>>>::deserialize(&mut deser)
.map_err(|e| format!("failed to deserialize output: {e}"))
}
fn provides(&self) -> std::result::Result<Vec<Item<'static>>, String> {
self.plugin().map_err(|e| e.to_string())?.provides_fn()?(self)
}

#[tracing::instrument]
fn requires(&self) -> std::result::Result<Vec<Requirement<'f>>, String> {
let mut cmd = self.base_cmd();
cmd.arg("requires");
trace!("running {cmd:?} to determine requires");
let out = cmd
.output()
.map_err(|e| format!("failed to run cmd {cmd:?}: {e}"))?;
if !out.status.success() {
Err(format!("{cmd:?} failed with exit code {}", out.status))
} else {
let mut deser = serde_json::Deserializer::from_reader(Cursor::new(&out.stdout));
<Vec<Requirement<'f>>>::deserialize(&mut deser)
.map_err(|e| format!("failed to deserialize output: {e}"))
}
fn requires(&self) -> std::result::Result<Vec<Requirement<'static>>, String> {
self.plugin().map_err(|e| e.to_string())?.requires_fn()?(self)
}
}
4 changes: 3 additions & 1 deletion antlir/antlir2/antlir2_features/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ rust_library(
name = "antlir2_features",
srcs = glob(["src/**/*.rs"]),
deps = [
"memfd",
"libloading",
"once_cell",
"serde",
"serde_json",
"thiserror",
"//antlir/buck/buck_label:buck_label",
],
)
Loading

0 comments on commit fd4d56a

Please sign in to comment.