repo_description: Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages
repo_topics: cffi cpython cross-compile hacktoberfest manylinux packaging pyo3 pypi pypy python uniffi wheels
code: use std::collections::HashMap;
  use std::path::Path;
  use std::path::PathBuf;
  use std::process::Command;
  
  use anyhow::Context as _;
  use anyhow::Result;
  use anyhow::bail;
  use fs_err as fs;
  use normpath::PathExt as _;
  use tracing::debug;
  
  use crate::BuildArtifact;
  use crate::BuildContext;
  use crate::archive_source::ArchiveSource;
  use crate::archive_source::FileSourceData;
  use crate::archive_source::GeneratedSourceData;
  use crate::binding_generator::ArtifactTarget;
  use crate::target::Os;
  
  use super::BindingGenerator;
  use super::GeneratorOutput;
  
  /// A generator for producing UniFFI bindings.
  #[derive(Default)]
  pub struct UniFfiBindingGenerator {}
  
  impl BindingGenerator for UniFfiBindingGenerator {
      fn generate_bindings(
          &mut self,
          context: &BuildContext,
          artifact: &BuildArtifact,
          module: &Path,
      ) -> Result<GeneratorOutput> {
          let base_path = if context.project.project_layout.python_module.is_some() {
              module.join(&context.project.project_layout.extension_name)
          } else {
              module.to_path_buf()
          };
  
          let UniFfiBindings {
              names: binding_names,
              cdylib,
              path: binding_dir,
          } = generate_uniffi_bindings(
              context.project.manifest_path.parent().unwrap(),
              &context.project.target_dir,
              &context.project.module_name,
              context.project.target.target_os(),
              &artifact.path,
          )?;
          let artifact_target = ArtifactTarget::ExtensionModule(base_path.join(cdylib));
          let mut additional_files = HashMap::new();
  
          let py_init = binding_names
              .iter()
              .map(|name| format!("from .{name} import *  # NOQA\n"))
              .collect::<Vec<String>>()
              .join("");
          additional_files.insert(
              base_path.join("__init__.py"),
              ArchiveSource::Generated(GeneratedSourceData {
                  data: py_init.into(),
                  path: None,
                  executable: false,
              }),
          );
  
          for binding in binding_names {
              let filename = format!("{binding}.py");
              let source = FileSourceData {
                  path: binding_dir.join(&filename),
                  executable: false,
              };
              additional_files.insert(base_path.join(filename), ArchiveSource::File(source));
          }
  
          Ok(GeneratorOutput {
              artifact_target,
              artifact_source_override: None,
              additional_files: Some(additional_files),
          })
      }
  }
  
  /// uniffi.toml
  #[derive(Debug, serde::Deserialize)]
  struct UniFfiToml {
      #[serde(default)]
      bindings: HashMap<String, UniFfiBindingsConfig>,
  }
  
  /// `bindings` section of uniffi.toml
  #[derive(Debug, serde::Deserialize)]
  struct UniFfiBindingsConfig {
      cdylib_name: Option<String>,
      /// Maps external crate names to their Python package names.
      /// Bindings for these crates are provided by external packages
      /// and should not be included in the wheel.
      #[serde(default)]
      external_packages: HashMap<String, String>,
  }
  
  #[derive(Debug, Clone)]
  struct UniFfiBindings {
      names: Vec<String>,
      cdylib: String,
      path: PathBuf,
  }
  
  fn uniffi_bindgen_command(crate_dir: &Path) -> Result<Command> {
      let manifest_path = crate_dir.join("Cargo.toml");
      let cargo_metadata = cargo_metadata::MetadataCommand::new()
          .manifest_path(&manifest_path)
          // We don't need to resolve the dependency graph
          .no_deps()
          .verbose(true)
          .exec()?;
      let root_pkg = match cargo_metadata.root_package() {
          Some(pkg) => Some(pkg),
          None => cargo_metadata
              .packages
              .iter()
              .find(|&p| p.manifest_path == manifest_path),
      };
  
      let uniffi_bindgen_target = root_pkg.and_then(|pkg| {
          pkg.targets
              .iter()
              .find(|&target| target.name == "uniffi-bindgen" && target.is_bin())
      });
      let uniffi_bindgen_workspace_target = cargo_metadata
          .packages
          .iter()
          .flat_map(|pkg| pkg.targets.iter())
          .find(|&target| target.name == "uniffi-bindgen" && target.is_bin());
  
      let command = if let Some(target) = uniffi_bindgen_target {
          let mut command = Command::new("cargo");
          command
              .args(["run", "--bin", "uniffi-bindgen", "--manifest-path"])
              .arg(manifest_path)
              .current_dir(crate_dir)
              .env_remove("CARGO_BUILD_TARGET");
          if !target.required_features.is_empty() {
              let features = target.required_features.join(",");
              command.arg("--features").arg(features);
          }
          command
      } else if let Some(target) = uniffi_bindgen_workspace_target {
          let mut command = Command::new("cargo");
          command
              .args(["run", "--bin", "uniffi-bindgen"])
              .current_dir(cargo_metadata.workspace_root)
              .env_remove("CARGO_BUILD_TARGET");
          if !target.required_features.is_empty() {
              let features = target.required_features.join(",");
              command.arg("--features").arg(features);
          }
          command
      } else {
          let mut command = Command::new("uniffi-bindgen");
          command.current_dir(crate_dir);
          command
      };
      Ok(command)
  }
  
  fn generate_uniffi_bindings(
      crate_dir: &Path,
      target_dir: &Path,
      module_name: &str,
      target_os: Os,
      artifact: &Path,
  ) -> Result<UniFfiBindings> {
      // `binding_dir` must use absolute path because we chdir to `crate_dir`
      // when running uniffi-bindgen
      let binding_dir = target_dir
          .normalize()?
          .join(env!("CARGO_PKG_NAME"))
          .join("uniffi")
          .join(module_name)
          .into_path_buf();
      fs::create_dir_all(&binding_dir)?;
  
      let mut cmd = uniffi_bindgen_command(crate_dir)?;
      cmd.args([
          "generate",
          "--no-format",
          "--language",
          "python",
          "--out-dir",
      ]);
      cmd.arg(&binding_dir);
  
      let config_file = crate_dir.join("uniffi.toml");
      let mut cdylib_name = None;
      let mut external_package_names = Vec::new();
      if config_file.is_file() {
          let uniffi_toml: UniFfiToml = toml::from_str(&fs::read_to_string(&config_file)?)?;
          if let Some(py_config) = uniffi_toml.bindings.get("python") {
              cdylib_name = py_config.cdylib_name.clone();
              external_package_names = py_config.external_packages.keys().cloned().collect();
          }
  
          // TODO: is this needed? `uniffi-bindgen` uses `uniffi.toml` by default,
          // `uniffi_bindgen_command` sets cwd to the crate (workspace) root, so maybe
          // we don't need to pass the config file explicitly?
          cmd.arg("--config");
          cmd.arg(config_file);
      }
  
      cmd.arg("--library");
      cmd.arg(artifact);
  
      debug!("Running {:?}", cmd);
      let mut child = cmd.spawn().context(
          "Failed to run uniffi-bindgen, did you install it? Try `pip install uniffi-bindgen`",
      )?;
      let exit_status = child.wait().context("Failed to run uniffi-bindgen")?;
      if !exit_status.success() {
          bail!("Command {:?} failed", cmd);
      }
  
      // Name of the cdylib is either from uniffi.toml or derived from the library
      let cdylib = match cdylib_name {
          // this logic should match with uniffi's expected names, e.g.
          // https://github.com/mozilla/uniffi-rs/blob/86a34083dd18bdd33f420c602b4fad624cc1e404/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py#L14-L37
          Some(cdylib_name) => super::cdylib_filename(&cdylib_name, target_os),
          None => artifact.file_name().unwrap().to_str().unwrap().to_string(),
      };
  
      let py_bindings: Vec<_> = fs::read_dir(&binding_dir)?
          .flatten()
          .filter(|file| file.path().extension().and_then(std::ffi::OsStr::to_str) == Some("py"))
          .map(|file| {
              file.path()
                  .file_stem()
                  .unwrap()
                  .to_string_lossy()
                  .to_string()
          })
          .filter(|name| !external_package_names.contains(name))
          .collect();
  
      if !external_package_names.is_empty() {
          debug!(
              "Excluding external UniFFI bindings: {:?}",
              external_package_names
          );
      }
  
      Ok(UniFfiBindings {
          names: py_bindings,
          cdylib,
          path: binding_dir,
      })
  }
  