async_cffi_codegen/
codegen_build.rs1use std::{
2 collections::HashMap,
3 fs,
4 io::{Error, ErrorKind, Result, Write as _},
5 path::Path,
6 process::{Command, Stdio},
7};
8
9use rs_schema::TraitSchema;
10
11use crate::{CodeGen as _, trait_to_async_cffi_py_schema, trait_to_async_cffi_rs_schema};
12
13pub fn generate_rs(
14 ios_trait_schema: &TraitSchema,
15 target_path: &Path,
16 supertrait_schemas: &HashMap<String, TraitSchema>,
17) -> Result<()> {
18 let file_schema = trait_to_async_cffi_rs_schema(ios_trait_schema, supertrait_schemas)
19 .map_err(|m| Error::new(ErrorKind::Other, m))?;
20 let new_contents = file_schema.codegen(0);
21 let formatted_new_contents = format_rust_code(&new_contents).unwrap_or_else(|err| {
22 println!("cargo:warning=Failed to format generated code: {err}");
23 new_contents
24 });
25 write_if_changed(&target_path, &formatted_new_contents)
26}
27
28fn format_rust_code(code: &str) -> Result<String> {
29 let edition = std::env::var("CARGO_PKG_EDITION").unwrap_or_else(|err| {
30 println!(
31 "cargo:warning=Using default edition 2024 for rustfmt (CARGO_PKG_EDITION missing: {err})"
32 );
33 "2024".to_string()
34 });
35
36 let mut process = Command::new("rustfmt")
37 .args(["--edition", &edition, "--emit", "stdout"])
38 .stdin(Stdio::piped())
39 .stdout(Stdio::piped())
40 .spawn()
41 .map_err(|e| Error::new(ErrorKind::Other, format!("failed to spawn rustfmt: {e}")))?;
42
43 if let Some(mut stdin) = process.stdin.take() {
44 stdin.write_all(code.as_bytes()).map_err(|e| {
45 Error::new(
46 ErrorKind::Other,
47 format!("failed to write to rustfmt stdin: {e}"),
48 )
49 })?;
50 }
51
52 let output = process.wait_with_output().map_err(|e| {
53 Error::new(
54 ErrorKind::Other,
55 format!("failed to read rustfmt output: {e}"),
56 )
57 })?;
58
59 if !output.status.success() {
60 return Err(Error::new(
61 ErrorKind::Other,
62 format!(
63 "rustfmt exited with status {}: {}",
64 output.status,
65 String::from_utf8_lossy(&output.stderr)
66 ),
67 ));
68 }
69
70 String::from_utf8(output.stdout).map_err(|e| {
71 Error::new(
72 ErrorKind::Other,
73 format!("rustfmt produced invalid UTF-8: {e}"),
74 )
75 })
76}
77
78fn format_python_code(code: &str) -> Result<String> {
79 let mut process = Command::new("python")
80 .args(["-m", "black", "-"])
81 .stdin(Stdio::piped())
82 .stdout(Stdio::piped())
83 .spawn()
84 .map_err(|e| Error::new(ErrorKind::Other, format!("failed to spawn black: {e}")))?;
85
86 if let Some(mut stdin) = process.stdin.take() {
87 stdin.write_all(code.as_bytes()).map_err(|e| {
88 Error::new(
89 ErrorKind::Other,
90 format!("failed to write to black stdin: {e}"),
91 )
92 })?;
93 }
94
95 let output = process.wait_with_output().map_err(|e| {
96 Error::new(
97 ErrorKind::Other,
98 format!("failed to read black output: {e}"),
99 )
100 })?;
101
102 if !output.status.success() {
103 return Err(Error::new(
104 ErrorKind::Other,
105 format!(
106 "black exited with status {}: {}",
107 output.status,
108 String::from_utf8_lossy(&output.stderr)
109 ),
110 ));
111 }
112
113 String::from_utf8(output.stdout).map_err(|e| {
114 Error::new(
115 ErrorKind::Other,
116 format!("black produced invalid UTF-8: {e}"),
117 )
118 })
119}
120
121pub fn generate_py(
122 ios_trait_schema: &TraitSchema,
123 target_path: &Path,
124 supertrait_schemas: &HashMap<String, TraitSchema>,
125) -> Result<()> {
126 let file_schema = trait_to_async_cffi_py_schema(ios_trait_schema, supertrait_schemas)
127 .map_err(|m| Error::new(ErrorKind::Other, m))?;
128
129 let new_contents = file_schema.codegen(0);
130 let formatted_new_contents = format_python_code(&new_contents).unwrap_or_else(|err| {
131 println!("cargo:warning=Failed to format generated Python code: {err}");
132 new_contents
133 });
134
135 write_if_changed(&target_path, &formatted_new_contents)
136}
137
138fn write_if_changed(target_path: &Path, contents: &str) -> Result<()> {
139 let normalized_new_contents = normalize_contents(contents);
140 let should_write = fs::read_to_string(target_path)
141 .map(|existing| normalize_contents(&existing) != normalized_new_contents)
142 .unwrap_or(true);
143
144 if should_write {
145 if let Some(parent) = target_path.parent() {
146 fs::create_dir_all(parent).map_err(|e| {
147 Error::new(
148 ErrorKind::Other,
149 format!(
150 "failed to create parent directories for {}: {e}",
151 parent.display()
152 ),
153 )
154 })?;
155 }
156
157 let mut file = fs::File::create(target_path).map_err(|e| {
158 Error::new(
159 ErrorKind::Other,
160 format!("failed to create file {}: {e}", target_path.display()),
161 )
162 })?;
163 file.write_all(normalized_new_contents.as_bytes())
164 .map_err(|e| {
165 Error::new(
166 ErrorKind::Other,
167 format!("failed to write file {}: {e}", target_path.display()),
168 )
169 })?;
170 file.sync_all().map_err(|e| {
171 Error::new(
172 ErrorKind::Other,
173 format!("failed to sync file {}: {e}", target_path.display()),
174 )
175 })?;
176 }
177
178 Ok(())
179}
180
181fn normalize_contents(contents: &str) -> String {
182 let mut normalized = contents.replace("\r\n", "\n");
183 if !normalized.ends_with('\n') {
184 normalized.push('\n');
185 }
186
187 normalized
188}