about summary refs log tree commit diff
path: root/tvix/nix-compat-derive
diff options
context:
space:
mode:
Diffstat (limited to 'tvix/nix-compat-derive')
-rw-r--r--tvix/nix-compat-derive/Cargo.toml32
-rw-r--r--tvix/nix-compat-derive/default.nix5
-rw-r--r--tvix/nix-compat-derive/src/de.rs272
-rw-r--r--tvix/nix-compat-derive/src/internal/attrs.rs358
-rw-r--r--tvix/nix-compat-derive/src/internal/ctx.rs50
-rw-r--r--tvix/nix-compat-derive/src/internal/inputs.rs110
-rw-r--r--tvix/nix-compat-derive/src/internal/mod.rs183
-rw-r--r--tvix/nix-compat-derive/src/internal/symbol.rs32
-rw-r--r--tvix/nix-compat-derive/src/lib.rs348
9 files changed, 1390 insertions, 0 deletions
diff --git a/tvix/nix-compat-derive/Cargo.toml b/tvix/nix-compat-derive/Cargo.toml
new file mode 100644
index 000000000000..dea8ab5ab159
--- /dev/null
+++ b/tvix/nix-compat-derive/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "nix-compat-derive"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+[features]
+external = []
+default = ["external"]
+
+
+[dependencies]
+proc-macro2 = { version = "1.0.86",  features = ["proc-macro"] }
+quote = { version = "1.0.36", features = ["proc-macro"] }
+syn = { version = "2.0.72", features = ["full", "extra-traits"] }
+
+[dev-dependencies]
+hex-literal = "0.4.1"
+pretty_assertions = "1.4.0"
+rstest = "0.19.0"
+tokio-test = "0.4.3"
+
+[dev-dependencies.tokio]
+version = "^1.38"
+features = ["io-util", "macros"]
+
+[dev-dependencies.nix-compat]
+path = "../nix-compat"
+default-features = false
+features = ["async", "wire", "test"]
diff --git a/tvix/nix-compat-derive/default.nix b/tvix/nix-compat-derive/default.nix
new file mode 100644
index 000000000000..e6636e7f2510
--- /dev/null
+++ b/tvix/nix-compat-derive/default.nix
@@ -0,0 +1,5 @@
+{ depot, lib, ... }:
+
+depot.tvix.crates.workspaceMembers.nix-compat-derive.build.override {
+  runTests = true;
+}
diff --git a/tvix/nix-compat-derive/src/de.rs b/tvix/nix-compat-derive/src/de.rs
new file mode 100644
index 000000000000..ee79ea9d1012
--- /dev/null
+++ b/tvix/nix-compat-derive/src/de.rs
@@ -0,0 +1,272 @@
+use proc_macro2::{Span, TokenStream};
+use quote::{quote, quote_spanned, ToTokens};
+use syn::spanned::Spanned;
+use syn::{DeriveInput, Generics, Path, Type};
+
+use crate::internal::attrs::Default;
+use crate::internal::inputs::RemoteInput;
+use crate::internal::{attrs, Container, Context, Data, Field, Remote, Style, Variant};
+
+pub fn expand_nix_deserialize(nnixrs: Path, input: &mut DeriveInput) -> syn::Result<TokenStream> {
+    let cx = Context::new();
+    let cont = Container::from_ast(&cx, nnixrs, input);
+    cx.check()?;
+    let cont = cont.unwrap();
+
+    let ty = cont.ident_type();
+    let body = nix_deserialize_body(&cont);
+    let crate_path = cont.crate_path();
+
+    Ok(nix_deserialize_impl(
+        crate_path,
+        &ty,
+        &cont.original.generics,
+        body,
+    ))
+}
+
+pub fn expand_nix_deserialize_remote(
+    crate_path: Path,
+    input: &RemoteInput,
+) -> syn::Result<TokenStream> {
+    let cx = Context::new();
+    let remote = Remote::from_ast(&cx, crate_path, input);
+    cx.check()?;
+    let remote = remote.unwrap();
+
+    let crate_path = remote.crate_path();
+    let body = nix_deserialize_body_from(crate_path, &remote.attrs).expect("From tokenstream");
+    let generics = Generics::default();
+    Ok(nix_deserialize_impl(crate_path, remote.ty, &generics, body))
+}
+
+fn nix_deserialize_impl(
+    crate_path: &Path,
+    ty: &Type,
+    generics: &Generics,
+    body: TokenStream,
+) -> TokenStream {
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+    quote! {
+        #[automatically_derived]
+        impl #impl_generics #crate_path::nix_daemon::de::NixDeserialize for #ty #ty_generics
+            #where_clause
+        {
+            #[allow(clippy::manual_async_fn)]
+            fn try_deserialize<R>(reader: &mut R) -> impl ::std::future::Future<Output=Result<Option<Self>, R::Error>> + Send + '_
+                where R: ?Sized + #crate_path::nix_daemon::de::NixRead + Send,
+            {
+                #body
+            }
+        }
+    }
+}
+
+fn nix_deserialize_body_from(
+    crate_path: &syn::Path,
+    attrs: &attrs::Container,
+) -> Option<TokenStream> {
+    if let Some(span) = attrs.from_str.as_ref() {
+        Some(nix_deserialize_from_str(crate_path, span.span()))
+    } else if let Some(type_from) = attrs.type_from.as_ref() {
+        Some(nix_deserialize_from(type_from))
+    } else {
+        attrs
+            .type_try_from
+            .as_ref()
+            .map(|type_try_from| nix_deserialize_try_from(crate_path, type_try_from))
+    }
+}
+
+fn nix_deserialize_body(cont: &Container) -> TokenStream {
+    if let Some(tokens) = nix_deserialize_body_from(cont.crate_path(), &cont.attrs) {
+        tokens
+    } else {
+        match &cont.data {
+            Data::Struct(style, fields) => nix_deserialize_struct(*style, fields),
+            Data::Enum(variants) => nix_deserialize_enum(variants),
+        }
+    }
+}
+
+fn nix_deserialize_struct(style: Style, fields: &[Field<'_>]) -> TokenStream {
+    let read_fields = fields.iter().map(|f| {
+        let field = f.var_ident();
+        let ty = f.ty;
+        let read_value = quote_spanned! {
+            ty.span()=> if first__ {
+                first__ = false;
+                if let Some(v) = reader.try_read_value::<#ty>().await? {
+                    v
+                } else {
+                    return Ok(None);
+                }
+            } else {
+                reader.read_value::<#ty>().await?
+            }
+        };
+        if let Some(version) = f.attrs.version.as_ref() {
+            let default = match &f.attrs.default {
+                Default::Default => quote_spanned!(ty.span()=>::std::default::Default::default),
+                Default::Path(path) => path.to_token_stream(),
+                _ => panic!("No default for versioned field"),
+            };
+            quote! {
+                let #field : #ty = if (#version).contains(&reader.version().minor()) {
+                    #read_value
+                } else {
+                    #default()
+                };
+            }
+        } else {
+            quote! {
+                let #field : #ty = #read_value;
+            }
+        }
+    });
+
+    let field_names = fields.iter().map(|f| f.var_ident());
+    let construct = match style {
+        Style::Struct => {
+            quote! {
+                Self { #(#field_names),* }
+            }
+        }
+        Style::Tuple => {
+            quote! {
+                Self(#(#field_names),*)
+            }
+        }
+        Style::Unit => quote!(Self),
+    };
+    quote! {
+        #[allow(unused_assignments)]
+        async move {
+            let mut first__ = true;
+            #(#read_fields)*
+            Ok(Some(#construct))
+        }
+    }
+}
+
+fn nix_deserialize_variant(variant: &Variant<'_>) -> TokenStream {
+    let ident = variant.ident;
+    let read_fields = variant.fields.iter().map(|f| {
+        let field = f.var_ident();
+        let ty = f.ty;
+        let read_value = quote_spanned! {
+            ty.span()=> if first__ {
+                first__ = false;
+                if let Some(v) = reader.try_read_value::<#ty>().await? {
+                    v
+                } else {
+                    return Ok(None);
+                }
+            } else {
+                reader.read_value::<#ty>().await?
+            }
+        };
+        if let Some(version) = f.attrs.version.as_ref() {
+            let default = match &f.attrs.default {
+                Default::Default => quote_spanned!(ty.span()=>::std::default::Default::default),
+                Default::Path(path) => path.to_token_stream(),
+                _ => panic!("No default for versioned field"),
+            };
+            quote! {
+                let #field : #ty = if (#version).contains(&reader.version().minor()) {
+                    #read_value
+                } else {
+                    #default()
+                };
+            }
+        } else {
+            quote! {
+                let #field : #ty = #read_value;
+            }
+        }
+    });
+    let field_names = variant.fields.iter().map(|f| f.var_ident());
+    let construct = match variant.style {
+        Style::Struct => {
+            quote! {
+                Self::#ident { #(#field_names),* }
+            }
+        }
+        Style::Tuple => {
+            quote! {
+                Self::#ident(#(#field_names),*)
+            }
+        }
+        Style::Unit => quote!(Self::#ident),
+    };
+    let version = &variant.attrs.version;
+    quote! {
+        #version => {
+            #(#read_fields)*
+            Ok(Some(#construct))
+        }
+    }
+}
+
+fn nix_deserialize_enum(variants: &[Variant<'_>]) -> TokenStream {
+    let match_variant = variants
+        .iter()
+        .map(|variant| nix_deserialize_variant(variant));
+    quote! {
+        #[allow(unused_assignments)]
+        async move {
+            let mut first__ = true;
+            match reader.version().minor() {
+                #(#match_variant)*
+            }
+        }
+    }
+}
+
+fn nix_deserialize_from(ty: &Type) -> TokenStream {
+    quote_spanned! {
+        ty.span() =>
+        async move {
+            if let Some(value) = reader.try_read_value::<#ty>().await? {
+                Ok(Some(<Self as ::std::convert::From<#ty>>::from(value)))
+            } else {
+                Ok(None)
+            }
+        }
+    }
+}
+
+fn nix_deserialize_try_from(crate_path: &Path, ty: &Type) -> TokenStream {
+    quote_spanned! {
+        ty.span() =>
+        async move {
+            use #crate_path::nix_daemon::de::Error;
+            if let Some(item) = reader.try_read_value::<#ty>().await? {
+                <Self as ::std::convert::TryFrom<#ty>>::try_from(item)
+                    .map_err(Error::invalid_data)
+                    .map(Some)
+            } else {
+                Ok(None)
+            }
+        }
+    }
+}
+
+fn nix_deserialize_from_str(crate_path: &Path, span: Span) -> TokenStream {
+    quote_spanned! {
+        span =>
+        async move {
+            use #crate_path::nix_daemon::de::Error;
+            if let Some(buf) = reader.try_read_bytes().await? {
+                let s = ::std::str::from_utf8(&buf)
+                    .map_err(Error::invalid_data)?;
+                <Self as ::std::str::FromStr>::from_str(s)
+                    .map_err(Error::invalid_data)
+                    .map(Some)
+            } else {
+                Ok(None)
+            }
+        }
+    }
+}
diff --git a/tvix/nix-compat-derive/src/internal/attrs.rs b/tvix/nix-compat-derive/src/internal/attrs.rs
new file mode 100644
index 000000000000..dbc959d1e917
--- /dev/null
+++ b/tvix/nix-compat-derive/src/internal/attrs.rs
@@ -0,0 +1,358 @@
+use quote::ToTokens;
+use syn::meta::ParseNestedMeta;
+use syn::parse::Parse;
+use syn::{parse_quote, Attribute, Expr, ExprLit, ExprPath, Lit, Token};
+
+use super::symbol::{Symbol, CRATE, DEFAULT, FROM, FROM_STR, NIX, TRY_FROM, VERSION};
+use super::Context;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum Default {
+    None,
+    #[allow(clippy::enum_variant_names)]
+    Default,
+    Path(ExprPath),
+}
+
+impl Default {
+    pub fn is_none(&self) -> bool {
+        matches!(self, Default::None)
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Field {
+    pub default: Default,
+    pub version: Option<syn::ExprRange>,
+}
+
+impl Field {
+    pub fn from_ast(ctx: &Context, attrs: &Vec<Attribute>) -> Field {
+        let mut version = None;
+        let mut default = Default::None;
+        for attr in attrs {
+            if attr.path() != NIX {
+                continue;
+            }
+            if let Err(err) = attr.parse_nested_meta(|meta| {
+                if meta.path == VERSION {
+                    version = parse_lit(ctx, &meta, VERSION)?;
+                } else if meta.path == DEFAULT {
+                    if meta.input.peek(Token![=]) {
+                        if let Some(path) = parse_lit(ctx, &meta, DEFAULT)? {
+                            default = Default::Path(path);
+                        }
+                    } else {
+                        default = Default::Default;
+                    }
+                } else {
+                    let path = meta.path.to_token_stream().to_string();
+                    return Err(meta.error(format_args!("unknown nix field attribute '{}'", path)));
+                }
+                Ok(())
+            }) {
+                eprintln!("{:?}", err.span().source_text());
+                ctx.syn_error(err);
+            }
+        }
+        if version.is_some() && default.is_none() {
+            default = Default::Default;
+        }
+
+        Field { default, version }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Variant {
+    pub version: syn::ExprRange,
+}
+
+impl Variant {
+    pub fn from_ast(ctx: &Context, attrs: &Vec<Attribute>) -> Variant {
+        let mut version = parse_quote!(..);
+        for attr in attrs {
+            if attr.path() != NIX {
+                continue;
+            }
+            if let Err(err) = attr.parse_nested_meta(|meta| {
+                if meta.path == VERSION {
+                    if let Some(v) = parse_lit(ctx, &meta, VERSION)? {
+                        version = v;
+                    }
+                } else {
+                    let path = meta.path.to_token_stream().to_string();
+                    return Err(
+                        meta.error(format_args!("unknown nix variant attribute '{}'", path))
+                    );
+                }
+                Ok(())
+            }) {
+                eprintln!("{:?}", err.span().source_text());
+                ctx.syn_error(err);
+            }
+        }
+
+        Variant { version }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Container {
+    pub from_str: Option<syn::Path>,
+    pub type_from: Option<syn::Type>,
+    pub type_try_from: Option<syn::Type>,
+    pub crate_path: Option<syn::Path>,
+}
+
+impl Container {
+    pub fn from_ast(ctx: &Context, attrs: &Vec<Attribute>) -> Container {
+        let mut type_from = None;
+        let mut type_try_from = None;
+        let mut crate_path = None;
+        let mut from_str = None;
+
+        for attr in attrs {
+            if attr.path() != NIX {
+                continue;
+            }
+            if let Err(err) = attr.parse_nested_meta(|meta| {
+                if meta.path == FROM {
+                    type_from = parse_lit(ctx, &meta, FROM)?;
+                } else if meta.path == TRY_FROM {
+                    type_try_from = parse_lit(ctx, &meta, TRY_FROM)?;
+                } else if meta.path == FROM_STR {
+                    from_str = Some(meta.path);
+                } else if meta.path == CRATE {
+                    crate_path = parse_lit(ctx, &meta, CRATE)?;
+                } else {
+                    let path = meta.path.to_token_stream().to_string();
+                    return Err(
+                        meta.error(format_args!("unknown nix variant attribute '{}'", path))
+                    );
+                }
+                Ok(())
+            }) {
+                eprintln!("{:?}", err.span().source_text());
+                ctx.syn_error(err);
+            }
+        }
+
+        Container {
+            from_str,
+            type_from,
+            type_try_from,
+            crate_path,
+        }
+    }
+}
+
+pub fn get_lit_str(
+    ctx: &Context,
+    meta: &ParseNestedMeta,
+    attr: Symbol,
+) -> syn::Result<Option<syn::LitStr>> {
+    let expr: Expr = meta.value()?.parse()?;
+    let mut value = &expr;
+    while let Expr::Group(e) = value {
+        value = &e.expr;
+    }
+    if let Expr::Lit(ExprLit {
+        lit: Lit::Str(s), ..
+    }) = value
+    {
+        Ok(Some(s.clone()))
+    } else {
+        ctx.error_spanned(
+            expr,
+            format_args!("expected nix attribute {} to be string", attr),
+        );
+        Ok(None)
+    }
+}
+
+pub fn parse_lit<T: Parse>(
+    ctx: &Context,
+    meta: &ParseNestedMeta,
+    attr: Symbol,
+) -> syn::Result<Option<T>> {
+    match get_lit_str(ctx, meta, attr)? {
+        Some(lit) => Ok(Some(lit.parse()?)),
+        None => Ok(None),
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use syn::{parse_quote, Attribute};
+
+    use crate::internal::Context;
+
+    use super::*;
+
+    #[test]
+    fn parse_field_version() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(version="..34")])];
+        let ctx = Context::new();
+        let field = Field::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            field,
+            Field {
+                default: Default::Default,
+                version: Some(parse_quote!(..34)),
+            }
+        );
+    }
+
+    #[test]
+    fn parse_field_default() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(default)])];
+        let ctx = Context::new();
+        let field = Field::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            field,
+            Field {
+                default: Default::Default,
+                version: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_field_default_path() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(default="Default::default")])];
+        let ctx = Context::new();
+        let field = Field::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            field,
+            Field {
+                default: Default::Path(parse_quote!(Default::default)),
+                version: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_field_both() {
+        let attrs: Vec<Attribute> =
+            vec![parse_quote!(#[nix(version="..", default="Default::default")])];
+        let ctx = Context::new();
+        let field = Field::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            field,
+            Field {
+                default: Default::Path(parse_quote!(Default::default)),
+                version: Some(parse_quote!(..)),
+            }
+        );
+    }
+
+    #[test]
+    fn parse_field_both_rev() {
+        let attrs: Vec<Attribute> =
+            vec![parse_quote!(#[nix(default="Default::default", version="..")])];
+        let ctx = Context::new();
+        let field = Field::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            field,
+            Field {
+                default: Default::Path(parse_quote!(Default::default)),
+                version: Some(parse_quote!(..)),
+            }
+        );
+    }
+
+    #[test]
+    fn parse_field_no_attr() {
+        let attrs: Vec<Attribute> = vec![];
+        let ctx = Context::new();
+        let field = Field::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            field,
+            Field {
+                default: Default::None,
+                version: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_field_no_subattrs() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix()])];
+        let ctx = Context::new();
+        let field = Field::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            field,
+            Field {
+                default: Default::None,
+                version: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_variant_version() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(version="..34")])];
+        let ctx = Context::new();
+        let variant = Variant::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            variant,
+            Variant {
+                version: parse_quote!(..34),
+            }
+        );
+    }
+
+    #[test]
+    fn parse_variant_no_attr() {
+        let attrs: Vec<Attribute> = vec![];
+        let ctx = Context::new();
+        let variant = Variant::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            variant,
+            Variant {
+                version: parse_quote!(..),
+            }
+        );
+    }
+
+    #[test]
+    fn parse_variant_no_subattrs() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix()])];
+        let ctx = Context::new();
+        let variant = Variant::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            variant,
+            Variant {
+                version: parse_quote!(..),
+            }
+        );
+    }
+
+    #[test]
+    fn parse_container_try_from() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(try_from="u64")])];
+        let ctx = Context::new();
+        let container = Container::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            container,
+            Container {
+                from_str: None,
+                type_from: None,
+                type_try_from: Some(parse_quote!(u64)),
+                crate_path: None,
+            }
+        );
+    }
+}
diff --git a/tvix/nix-compat-derive/src/internal/ctx.rs b/tvix/nix-compat-derive/src/internal/ctx.rs
new file mode 100644
index 000000000000..ba770e044bc2
--- /dev/null
+++ b/tvix/nix-compat-derive/src/internal/ctx.rs
@@ -0,0 +1,50 @@
+use std::cell::RefCell;
+use std::fmt;
+use std::thread::panicking;
+
+use quote::ToTokens;
+
+pub struct Context {
+    errors: RefCell<Option<Vec<syn::Error>>>,
+}
+
+impl Context {
+    pub fn new() -> Context {
+        Context {
+            errors: RefCell::new(Some(Vec::new())),
+        }
+    }
+
+    pub fn syn_error(&self, error: syn::Error) {
+        self.errors
+            .borrow_mut()
+            .as_mut()
+            .take()
+            .unwrap()
+            .push(error);
+    }
+
+    pub fn error_spanned<T: ToTokens, D: fmt::Display>(&self, tokens: T, message: D) {
+        self.syn_error(syn::Error::new_spanned(tokens, message));
+    }
+
+    pub fn check(&self) -> syn::Result<()> {
+        let mut iter = self.errors.borrow_mut().take().unwrap().into_iter();
+        let mut err = match iter.next() {
+            None => return Ok(()),
+            Some(err) => err,
+        };
+        for next_err in iter {
+            err.combine(next_err);
+        }
+        Err(err)
+    }
+}
+
+impl Drop for Context {
+    fn drop(&mut self) {
+        if self.errors.borrow().is_some() && !panicking() {
+            panic!("Context dropped without checking errors");
+        }
+    }
+}
diff --git a/tvix/nix-compat-derive/src/internal/inputs.rs b/tvix/nix-compat-derive/src/internal/inputs.rs
new file mode 100644
index 000000000000..097a141a5d7c
--- /dev/null
+++ b/tvix/nix-compat-derive/src/internal/inputs.rs
@@ -0,0 +1,110 @@
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RemoteInput {
+    pub attrs: Vec<syn::Attribute>,
+    pub ident: syn::Type,
+}
+
+impl syn::parse::Parse for RemoteInput {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let attrs = input.call(syn::Attribute::parse_outer)?;
+
+        let ident = input.parse::<syn::Type>()?;
+        Ok(RemoteInput { attrs, ident })
+    }
+}
+
+impl quote::ToTokens for RemoteInput {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        fn is_outer(attr: &&syn::Attribute) -> bool {
+            match attr.style {
+                syn::AttrStyle::Outer => true,
+                syn::AttrStyle::Inner(_) => false,
+            }
+        }
+        for attr in self.attrs.iter().filter(is_outer) {
+            attr.to_tokens(tokens);
+        }
+        self.ident.to_tokens(tokens);
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use syn::parse_quote;
+    //use syn::parse::Parse;
+
+    use super::*;
+
+    #[test]
+    fn test_input() {
+        let p: RemoteInput = parse_quote!(u64);
+        assert_eq!(
+            p,
+            RemoteInput {
+                attrs: vec![],
+                ident: parse_quote!(u64),
+            }
+        );
+    }
+
+    #[test]
+    fn test_input_attr() {
+        let p: RemoteInput = parse_quote!(
+            #[nix]
+            u64
+        );
+        assert_eq!(
+            p,
+            RemoteInput {
+                attrs: vec![parse_quote!(#[nix])],
+                ident: parse_quote!(u64),
+            }
+        );
+    }
+
+    #[test]
+    fn test_input_attr_multiple() {
+        let p: RemoteInput = parse_quote!(
+            #[nix]
+            #[hello]
+            u64
+        );
+        assert_eq!(
+            p,
+            RemoteInput {
+                attrs: vec![parse_quote!(#[nix]), parse_quote!(#[hello])],
+                ident: parse_quote!(u64),
+            }
+        );
+    }
+
+    #[test]
+    fn test_input_attr_full() {
+        let p: RemoteInput = parse_quote!(
+            #[nix(try_from = "u64")]
+            usize
+        );
+        assert_eq!(
+            p,
+            RemoteInput {
+                attrs: vec![parse_quote!(#[nix(try_from="u64")])],
+                ident: parse_quote!(usize),
+            }
+        );
+    }
+
+    #[test]
+    fn test_input_attr_other() {
+        let p: RemoteInput = parse_quote!(
+            #[muh]
+            u64
+        );
+        assert_eq!(
+            p,
+            RemoteInput {
+                attrs: vec![parse_quote!(#[muh])],
+                ident: parse_quote!(u64),
+            }
+        );
+    }
+}
diff --git a/tvix/nix-compat-derive/src/internal/mod.rs b/tvix/nix-compat-derive/src/internal/mod.rs
new file mode 100644
index 000000000000..20b243221619
--- /dev/null
+++ b/tvix/nix-compat-derive/src/internal/mod.rs
@@ -0,0 +1,183 @@
+use syn::punctuated::Punctuated;
+use syn::spanned::Spanned;
+use syn::Token;
+
+pub mod attrs;
+mod ctx;
+pub mod inputs;
+mod symbol;
+
+pub use ctx::Context;
+
+pub struct Field<'a> {
+    pub member: syn::Member,
+    pub ty: &'a syn::Type,
+    pub attrs: attrs::Field,
+    pub original: &'a syn::Field,
+}
+
+impl<'a> Field<'a> {
+    pub fn from_ast(ctx: &Context, idx: usize, field: &'a syn::Field) -> Field<'a> {
+        let attrs = attrs::Field::from_ast(ctx, &field.attrs);
+        let member = match &field.ident {
+            Some(id) => syn::Member::Named(id.clone()),
+            None => syn::Member::Unnamed(idx.into()),
+        };
+        Field {
+            member,
+            attrs,
+            ty: &field.ty,
+            original: field,
+        }
+    }
+
+    pub fn var_ident(&self) -> syn::Ident {
+        match &self.member {
+            syn::Member::Named(name) => name.clone(),
+            syn::Member::Unnamed(idx) => {
+                syn::Ident::new(&format!("field{}", idx.index), self.original.span())
+            }
+        }
+    }
+}
+
+pub struct Variant<'a> {
+    pub ident: &'a syn::Ident,
+    pub attrs: attrs::Variant,
+    pub style: Style,
+    pub fields: Vec<Field<'a>>,
+    //pub original: &'a syn::Variant,
+}
+
+impl<'a> Variant<'a> {
+    pub fn from_ast(ctx: &Context, variant: &'a syn::Variant) -> Self {
+        let attrs = attrs::Variant::from_ast(ctx, &variant.attrs);
+        let (style, fields) = match &variant.fields {
+            syn::Fields::Named(fields) => (Style::Struct, fields_ast(ctx, &fields.named)),
+            syn::Fields::Unnamed(fields) => (Style::Tuple, fields_ast(ctx, &fields.unnamed)),
+            syn::Fields::Unit => (Style::Unit, Vec::new()),
+        };
+        Variant {
+            ident: &variant.ident,
+            attrs,
+            style,
+            fields,
+            //original: variant,
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum Style {
+    Struct,
+    Tuple,
+    Unit,
+}
+
+pub enum Data<'a> {
+    Enum(Vec<Variant<'a>>),
+    Struct(Style, Vec<Field<'a>>),
+}
+
+pub struct Container<'a> {
+    pub ident: &'a syn::Ident,
+    pub attrs: attrs::Container,
+    pub data: Data<'a>,
+    pub crate_path: syn::Path,
+    pub original: &'a syn::DeriveInput,
+}
+
+impl<'a> Container<'a> {
+    pub fn from_ast(
+        ctx: &Context,
+        crate_path: syn::Path,
+        input: &'a mut syn::DeriveInput,
+    ) -> Option<Container<'a>> {
+        let attrs = attrs::Container::from_ast(ctx, &input.attrs);
+        let data = match &input.data {
+            syn::Data::Struct(s) => match &s.fields {
+                syn::Fields::Named(fields) => {
+                    Data::Struct(Style::Struct, fields_ast(ctx, &fields.named))
+                }
+                syn::Fields::Unnamed(fields) => {
+                    Data::Struct(Style::Tuple, fields_ast(ctx, &fields.unnamed))
+                }
+                syn::Fields::Unit => Data::Struct(Style::Unit, Vec::new()),
+            },
+            syn::Data::Enum(e) => {
+                let variants = e
+                    .variants
+                    .iter()
+                    .map(|variant| Variant::from_ast(ctx, variant))
+                    .collect();
+                Data::Enum(variants)
+            }
+            syn::Data::Union(u) => {
+                ctx.error_spanned(u.union_token, "Union not supported by nixrs");
+                return None;
+            }
+        };
+        Some(Container {
+            ident: &input.ident,
+            attrs,
+            data,
+            crate_path,
+            original: input,
+        })
+    }
+
+    pub fn crate_path(&self) -> &syn::Path {
+        if let Some(crate_path) = self.attrs.crate_path.as_ref() {
+            crate_path
+        } else {
+            &self.crate_path
+        }
+    }
+
+    pub fn ident_type(&self) -> syn::Type {
+        let path: syn::Path = self.ident.clone().into();
+        let tp = syn::TypePath { qself: None, path };
+        tp.into()
+    }
+}
+
+pub struct Remote<'a> {
+    pub attrs: attrs::Container,
+    pub ty: &'a syn::Type,
+    pub crate_path: syn::Path,
+}
+
+impl<'a> Remote<'a> {
+    pub fn from_ast(
+        ctx: &Context,
+        crate_path: syn::Path,
+        input: &'a inputs::RemoteInput,
+    ) -> Option<Remote<'a>> {
+        let attrs = attrs::Container::from_ast(ctx, &input.attrs);
+        if attrs.from_str.is_none() && attrs.type_from.is_none() && attrs.type_try_from.is_none() {
+            ctx.error_spanned(input, "Missing from_str, from or try_from attribute");
+            return None;
+        }
+        Some(Remote {
+            ty: &input.ident,
+            attrs,
+            crate_path,
+        })
+    }
+
+    pub fn crate_path(&self) -> &syn::Path {
+        if let Some(crate_path) = self.attrs.crate_path.as_ref() {
+            crate_path
+        } else {
+            &self.crate_path
+        }
+    }
+}
+
+fn fields_ast<'a>(ctx: &Context, fields: &'a Punctuated<syn::Field, Token![,]>) -> Vec<Field<'a>> {
+    fields
+        .iter()
+        .enumerate()
+        .map(|(idx, field)| Field::from_ast(ctx, idx, field))
+        .collect()
+}
diff --git a/tvix/nix-compat-derive/src/internal/symbol.rs b/tvix/nix-compat-derive/src/internal/symbol.rs
new file mode 100644
index 000000000000..ed3fe304eb5d
--- /dev/null
+++ b/tvix/nix-compat-derive/src/internal/symbol.rs
@@ -0,0 +1,32 @@
+use std::fmt;
+
+use syn::Path;
+
+#[derive(Copy, Clone)]
+pub struct Symbol(&'static str);
+
+pub const NIX: Symbol = Symbol("nix");
+pub const VERSION: Symbol = Symbol("version");
+pub const DEFAULT: Symbol = Symbol("default");
+pub const FROM: Symbol = Symbol("from");
+pub const TRY_FROM: Symbol = Symbol("try_from");
+pub const FROM_STR: Symbol = Symbol("from_str");
+pub const CRATE: Symbol = Symbol("crate");
+
+impl PartialEq<Symbol> for Path {
+    fn eq(&self, word: &Symbol) -> bool {
+        self.is_ident(word.0)
+    }
+}
+
+impl<'a> PartialEq<Symbol> for &'a Path {
+    fn eq(&self, word: &Symbol) -> bool {
+        self.is_ident(word.0)
+    }
+}
+
+impl fmt::Display for Symbol {
+    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str(self.0)
+    }
+}
diff --git a/tvix/nix-compat-derive/src/lib.rs b/tvix/nix-compat-derive/src/lib.rs
new file mode 100644
index 000000000000..5c0dd5c2d480
--- /dev/null
+++ b/tvix/nix-compat-derive/src/lib.rs
@@ -0,0 +1,348 @@
+//! # Using derive
+//!
+//! 1. [Overview](#overview)
+//! 3. [Attributes](#attributes)
+//!     1. [Container attributes](#container-attributes)
+//!         1. [`#[nix(from_str)]`](#nixfrom_str)
+//!         2. [`#[nix(from = "FromType")]`](#nixfrom--fromtype)
+//!         3. [`#[nix(try_from = "FromType")]`](#nixtry_from--fromtype)
+//!         4. [`#[nix(crate = "...")]`](#nixcrate--)
+//!     2. [Variant attributes](#variant-attributes)
+//!         1. [`#[nix(version = "range")]`](#nixversion--range)
+//!     3. [Field attributes](#field-attributes)
+//!         1. [`#[nix(version = "range")]`](#nixversion--range-1)
+//!         2. [`#[nix(default)]`](#nixdefault)
+//!         3. [`#[nix(default = "path")]`](#nixdefault--path)
+//!
+//! ## Overview
+//!
+//! This crate contains derive macros and function-like macros for implementing
+//! `NixDeserialize` with less boilerplate.
+//!
+//! ### Examples
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! struct Unnamed(u64, String);
+//! ```
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! struct Fields {
+//!     number: u64,
+//!     message: String,
+//! };
+//! ```
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! struct Ignored;
+//! ```
+//!
+//! ## Attributes
+//!
+//! To customize the derived trait implementations you can add
+//! [attributes](https://doc.rust-lang.org/reference/attributes.html)
+//! to containers, fields and variants.
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! #[nix(crate="nix_compat")] // <-- This is a container attribute
+//! struct Fields {
+//!     number: u64,
+//!     #[nix(version="..20")] // <-- This is a field attribute
+//!     message: String,
+//! };
+//!
+//! #[derive(NixDeserialize)]
+//! #[nix(crate="nix_compat")] // <-- This is also a container attribute
+//! enum E {
+//!     #[nix(version="..10")] // <-- This is a variant attribute
+//!     A(u64),
+//!     #[nix(version="10..")] // <-- This is also a variant attribute
+//!     B(String),
+//! }
+//! ```
+//!
+//! ### Container attributes
+//!
+//! ##### `#[nix(from_str)]`
+//!
+//! When `from_str` is specified the fields are all ignored and instead a
+//! `String` is first deserialized and then `FromStr::from_str` is used
+//! to convert this `String` to the container type.
+//!
+//! This means that the container must implement `FromStr` and the error
+//! returned from the `from_str` must implement `Display`.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! #[nix(from_str)]
+//! struct MyString(String);
+//! impl std::str::FromStr for MyString {
+//!     type Err = String;
+//!     fn from_str(s: &str) -> Result<Self, Self::Err> {
+//!         if s != "bad string" {
+//!             Ok(MyString(s.to_string()))
+//!         } else {
+//!             Err("Got a bad string".to_string())
+//!         }
+//!     }
+//! }
+//! ```
+//!
+//! ##### `#[nix(from = "FromType")]`
+//!
+//! When `from` is specified the fields are all ignored and instead a
+//! value of `FromType` is first deserialized and then `From::from` is
+//! used to convert from this value to the container type.
+//!
+//! This means that the container must implement `From<FromType>` and
+//! `FromType` must implement `NixDeserialize`.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! #[nix(from="usize")]
+//! struct MyValue(usize);
+//! impl From<usize> for MyValue {
+//!     fn from(val: usize) -> Self {
+//!         MyValue(val)
+//!     }
+//! }
+//! ```
+//!
+//! ##### `#[nix(try_from = "FromType")]`
+//!
+//! With `try_from` a value of `FromType` is first deserialized and then
+//! `TryFrom::try_from` is used to convert from this value to the container
+//! type.
+//!
+//! This means that the container must implement `TryFrom<FromType>` and
+//! `FromType` must implement `NixDeserialize`.
+//! The error returned from `try_from` also needs to implement `Display`.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! #[nix(try_from="usize")]
+//! struct WrongAnswer(usize);
+//! impl TryFrom<usize> for WrongAnswer {
+//!     type Error = String;
+//!     fn try_from(val: usize) -> Result<Self, Self::Error> {
+//!         if val != 42 {
+//!             Ok(WrongAnswer(val))
+//!         } else {
+//!             Err("Got the answer to life the universe and everything".to_string())
+//!         }
+//!     }
+//! }
+//! ```
+//!
+//! ##### `#[nix(crate = "...")]`
+//!
+//! Specify the path to the `nix-compat` crate instance to use when referring
+//! to the API in the generated code. This is usually not needed.
+//!
+//! ### Variant attributes
+//!
+//! ##### `#[nix(version = "range")]`
+//!
+//! Specifies the protocol version range where this variant is used.
+//! When deriving an enum the `version` attribute is used to select which
+//! variant of the enum to deserialize. The range is for minor version and
+//! the version ranges of all variants combined must cover all versions
+//! without any overlap or the first variant that matches is selected.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #[derive(NixDeserialize)]
+//! enum Testing {
+//!     #[nix(version="..=18")]
+//!     OldVersion(u64),
+//!     #[nix(version="19..")]
+//!     NewVersion(String),
+//! }
+//! ```
+//!
+//! ### Field attributes
+//!
+//! ##### `#[nix(version = "range")]`
+//!
+//! Specifies the protocol version range where this field is included.
+//! The range is for minor version. For example `version = "..20"`
+//! includes the field in protocol versions `1.0` to `1.19` and skips
+//! it in version `1.20` and above.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! struct Field {
+//!     number: u64,
+//!     #[nix(version="..20")]
+//!     messsage: String,
+//! }
+//! ```
+//!
+//! ##### `#[nix(default)]`
+//!
+//! When a field is skipped because the active protocol version falls
+//! outside the range specified in [`#[nix(version = "range")]`](#nixversion--range-1)
+//! this attribute indicates that `Default::default()` should be used
+//! to get a value for the field. This is also the default
+//! when you only specify [`#[nix(version = "range")]`](#nixversion--range-1).
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! struct Field {
+//!     number: u64,
+//!     #[nix(version="..20", default)]
+//!     messsage: String,
+//! }
+//! ```
+//!
+//! ##### `#[nix(default = "path")]`
+//!
+//! When a field is skipped because the active protocol version falls
+//! outside the range specified in [`#[nix(version = "range")]`](#nixversion--range-1)
+//! this attribute indicates that the function in `path` should be called to
+//! get a default value for the field. The given function must be callable
+//! as `fn() -> T`.
+//! For example `default = "my_value"` would call `my_value()` and `default =
+//! "AType::empty"` would call `AType::empty()`.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixDeserialize;
+//! #
+//! #[derive(NixDeserialize)]
+//! struct Field {
+//!     number: u64,
+//!     #[nix(version="..20", default="missing_string")]
+//!     messsage: String,
+//! }
+//!
+//! fn missing_string() -> String {
+//!     "missing string".to_string()
+//! }
+//! ```
+
+use internal::inputs::RemoteInput;
+use proc_macro::TokenStream;
+use syn::{parse_quote, DeriveInput};
+
+mod de;
+mod internal;
+
+#[cfg(not(feature = "external"))]
+#[proc_macro_derive(NixDeserialize, attributes(nix))]
+pub fn derive_nix_deserialize(item: TokenStream) -> TokenStream {
+    let mut input = syn::parse_macro_input!(item as DeriveInput);
+    let nnixrs: syn::Path = parse_quote!(crate);
+    de::expand_nix_deserialize(nnixrs, &mut input)
+        .unwrap_or_else(syn::Error::into_compile_error)
+        .into()
+}
+
+#[cfg(feature = "external")]
+#[proc_macro_derive(NixDeserialize, attributes(nix))]
+pub fn derive_nix_deserialize(item: TokenStream) -> TokenStream {
+    let mut input = syn::parse_macro_input!(item as DeriveInput);
+    let nnixrs: syn::Path = parse_quote!(::nix_compat);
+    de::expand_nix_deserialize(nnixrs, &mut input)
+        .unwrap_or_else(syn::Error::into_compile_error)
+        .into()
+}
+
+/// Macro to implement `NixDeserialize` on a type.
+/// Sometimes you can't use the deriver to implement `NixDeserialize`
+/// (like when dealing with types in Rust standard library) but don't want
+/// to implement it yourself. So this macro can be used for those situations
+/// where you would derive using `#[nix(from_str)]`,
+/// `#[nix(from = "FromType")]` or `#[nix(try_from = "FromType")]` if you
+/// could.
+///
+/// #### Example
+///
+/// ```rust
+/// # use nix_compat_derive::nix_deserialize_remote;
+/// #
+/// struct MyU64(u64);
+///
+/// impl From<u64> for MyU64 {
+///     fn from(value: u64) -> Self {
+///         Self(value)
+///     }
+/// }
+///
+/// nix_deserialize_remote!(#[nix(from="u64")] MyU64);
+/// ```
+#[cfg(not(feature = "external"))]
+#[proc_macro]
+pub fn nix_deserialize_remote(item: TokenStream) -> TokenStream {
+    let input = syn::parse_macro_input!(item as RemoteInput);
+    let crate_path = parse_quote!(crate);
+    de::expand_nix_deserialize_remote(crate_path, &input)
+        .unwrap_or_else(syn::Error::into_compile_error)
+        .into()
+}
+
+/// Macro to implement `NixDeserialize` on a type.
+/// Sometimes you can't use the deriver to implement `NixDeserialize`
+/// (like when dealing with types in Rust standard library) but don't want
+/// to implement it yourself. So this macro can be used for those situations
+/// where you would derive using `#[nix(from_str)]`,
+/// `#[nix(from = "FromType")]` or `#[nix(try_from = "FromType")]` if you
+/// could.
+///
+/// #### Example
+///
+/// ```rust
+/// # use nix_compat_derive::nix_deserialize_remote;
+/// #
+/// struct MyU64(u64);
+///
+/// impl From<u64> for MyU64 {
+///     fn from(value: u64) -> Self {
+///         Self(value)
+///     }
+/// }
+///
+/// nix_deserialize_remote!(#[nix(from="u64")] MyU64);
+/// ```
+#[cfg(feature = "external")]
+#[proc_macro]
+pub fn nix_deserialize_remote(item: TokenStream) -> TokenStream {
+    let input = syn::parse_macro_input!(item as RemoteInput);
+    let crate_path = parse_quote!(::nix_compat);
+    de::expand_nix_deserialize_remote(crate_path, &input)
+        .unwrap_or_else(syn::Error::into_compile_error)
+        .into()
+}