1use std::fs;
2use std::io::{Error, Write};
3use std::path::Path;
4
5use clap::{ArgEnum, Args};
6use fe_common::diagnostics::print_diagnostics;
7use fe_common::files::SourceFileId;
8use fe_common::utils::files::{get_project_root, BuildFiles, ProjectMode};
9use fe_driver::CompiledModule;
10
11const DEFAULT_OUTPUT_DIR_NAME: &str = "output";
12
13#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum, Debug)]
14enum Emit {
15 Abi,
16 Ast,
17 LoweredAst,
18 Bytecode,
19 RuntimeBytecode,
20 Tokens,
21 Yul,
22}
23
24#[derive(Args)]
25#[clap(about = "Build the current project")]
26pub struct BuildArgs {
27 #[clap(default_value_t = get_project_root().unwrap_or(".".to_string()))]
28 input_path: String,
29 #[clap(short, long, default_value = DEFAULT_OUTPUT_DIR_NAME)]
30 output_dir: String,
31 #[clap(
32 arg_enum,
33 use_value_delimiter = true,
34 long,
35 short,
36 default_value = "abi,bytecode"
37 )]
38 emit: Vec<Emit>,
39 #[clap(long)]
40 mir: bool,
41 #[clap(long)]
42 overwrite: bool,
43 #[clap(long, takes_value(true))]
44 optimize: Option<bool>,
45}
46
47fn build_single_file(compile_arg: &BuildArgs) -> (String, CompiledModule) {
48 let emit = &compile_arg.emit;
49 let with_bytecode = emit.contains(&Emit::Bytecode);
50 let with_runtime_bytecode = emit.contains(&Emit::RuntimeBytecode);
51 let input_path = &compile_arg.input_path;
52 let optimize = compile_arg.optimize.unwrap_or(true);
53
54 let mut db = fe_driver::Db::default();
55 let content = match std::fs::read_to_string(input_path) {
56 Err(err) => {
57 eprintln!("Failed to load file: `{input_path}`. Error: {err}");
58 std::process::exit(1)
59 }
60 Ok(content) => content,
61 };
62
63 let compiled_module = match fe_driver::compile_single_file(
64 &mut db,
65 input_path,
66 &content,
67 with_bytecode,
68 with_runtime_bytecode,
69 optimize,
70 ) {
71 Ok(module) => module,
72 Err(error) => {
73 eprintln!("Unable to compile {input_path}.");
74 print_diagnostics(&db, &error.0);
75 std::process::exit(1)
76 }
77 };
78 (content, compiled_module)
79}
80
81fn build_ingot(compile_arg: &BuildArgs) -> (String, CompiledModule) {
82 let emit = &compile_arg.emit;
83 let with_bytecode = emit.contains(&Emit::Bytecode);
84 let with_runtime_bytecode = emit.contains(&Emit::RuntimeBytecode);
85 let input_path = &compile_arg.input_path;
86 let optimize = compile_arg.optimize.unwrap_or(true);
87
88 if !Path::new(input_path).exists() {
89 eprintln!("Input directory does not exist: `{input_path}`.");
90 std::process::exit(1)
91 }
92
93 let build_files = match BuildFiles::load_fs(input_path) {
94 Ok(files) => files,
95 Err(err) => {
96 eprintln!("Failed to load project files.\nError: {err}");
97 std::process::exit(1)
98 }
99 };
100
101 if build_files.root_project_mode() == ProjectMode::Lib {
102 eprintln!("Unable to compile {input_path}. No build targets in library mode.");
103 eprintln!("Consider replacing `src/lib.fe` with `src/main.fe`.");
104 std::process::exit(1)
105 }
106
107 let mut db = fe_driver::Db::default();
108 let compiled_module = match fe_driver::compile_ingot(
109 &mut db,
110 &build_files,
111 with_bytecode,
112 with_runtime_bytecode,
113 optimize,
114 ) {
115 Ok(module) => module,
116 Err(error) => {
117 eprintln!("Unable to compile {input_path}.");
118 print_diagnostics(&db, &error.0);
119 std::process::exit(1)
120 }
121 };
122
123 ("".to_string(), compiled_module)
125}
126
127pub fn build(compile_arg: BuildArgs) {
128 let emit = &compile_arg.emit;
129
130 let input_path = &compile_arg.input_path;
131
132 if compile_arg.mir {
133 return mir_dump(input_path);
134 }
135
136 let _with_bytecode = emit.contains(&Emit::Bytecode);
137 #[cfg(not(feature = "solc-backend"))]
138 if _with_bytecode {
139 eprintln!("Warning: bytecode output requires 'solc-backend' feature. Try `cargo build --release --features solc-backend`. Skipping.");
140 }
141
142 let (content, compiled_module) = if Path::new(input_path).is_file() {
143 build_single_file(&compile_arg)
144 } else {
145 build_ingot(&compile_arg)
146 };
147
148 let output_dir = &compile_arg.output_dir;
149 let overwrite = compile_arg.overwrite;
150 match write_compiled_module(compiled_module, &content, emit, output_dir, overwrite) {
151 Ok(_) => eprintln!("Compiled {input_path}. Outputs in `{output_dir}`"),
152 Err(err) => {
153 eprintln!("Failed to write output to directory: `{output_dir}`. Error: {err}");
154 std::process::exit(1)
155 }
156 }
157}
158
159fn write_compiled_module(
160 mut module: CompiledModule,
161 file_content: &str,
162 targets: &[Emit],
163 output_dir: &str,
164 overwrite: bool,
165) -> Result<(), String> {
166 let output_dir = Path::new(output_dir);
167 if output_dir.is_file() {
168 return Err(format!(
169 "A file exists at path `{}`, the location of the output directory. Refusing to overwrite.",
170 output_dir.display()
171 ));
172 }
173
174 if !overwrite {
175 verify_nonexistent_or_empty(output_dir)?;
176 }
177
178 fs::create_dir_all(output_dir).map_err(ioerr_to_string)?;
179
180 if targets.contains(&Emit::Ast) {
181 write_output(&output_dir.join("module.ast"), &module.src_ast)?;
182 }
183
184 if targets.contains(&Emit::LoweredAst) {
185 write_output(&output_dir.join("lowered_module.ast"), &module.lowered_ast)?;
186 }
187
188 if targets.contains(&Emit::Tokens) {
189 let tokens = {
190 let lexer = fe_parser::lexer::Lexer::new(SourceFileId::dummy_file(), file_content);
191 lexer.collect::<Vec<_>>()
192 };
193 write_output(&output_dir.join("module.tokens"), &format!("{tokens:#?}"))?;
194 }
195
196 for (name, contract) in module.contracts.drain(0..) {
197 let contract_output_dir = output_dir.join(&name);
198 fs::create_dir_all(&contract_output_dir).map_err(ioerr_to_string)?;
199
200 if targets.contains(&Emit::Abi) {
201 let file_name = format!("{}_abi.json", &name);
202 write_output(&contract_output_dir.join(file_name), &contract.json_abi)?;
203 }
204
205 if targets.contains(&Emit::Yul) {
206 let file_name = format!("{}_ir.yul", &name);
207 write_output(&contract_output_dir.join(file_name), &contract.yul)?;
208 }
209
210 #[cfg(feature = "solc-backend")]
211 if targets.contains(&Emit::Bytecode) {
212 let file_name = format!("{}.bin", &name);
213 write_output(&contract_output_dir.join(file_name), &contract.bytecode)?;
214 }
215 #[cfg(feature = "solc-backend")]
216 if targets.contains(&Emit::RuntimeBytecode) {
217 let file_name = format!("{}.runtime.bin", &name);
218 write_output(
219 &contract_output_dir.join(file_name),
220 &contract.runtime_bytecode,
221 )?;
222 }
223 }
224
225 Ok(())
226}
227
228fn write_output(path: &Path, content: &str) -> Result<(), String> {
229 let mut file = fs::OpenOptions::new()
230 .write(true)
231 .create(true)
232 .truncate(true)
233 .open(path)
234 .map_err(ioerr_to_string)?;
235 file.write_all(content.as_bytes())
236 .map_err(ioerr_to_string)?;
237 Ok(())
238}
239
240fn ioerr_to_string(error: Error) -> String {
241 format!("{error}")
242}
243
244fn verify_nonexistent_or_empty(dir: &Path) -> Result<(), String> {
245 if !dir.exists() || dir.read_dir().map_err(ioerr_to_string)?.next().is_none() {
246 Ok(())
247 } else {
248 Err(format!(
249 "Directory '{}' is not empty. Use --overwrite to overwrite.",
250 dir.display()
251 ))
252 }
253}
254
255fn mir_dump(input_path: &str) {
256 let mut db = fe_driver::Db::default();
257 if Path::new(input_path).is_file() {
258 let content = match std::fs::read_to_string(input_path) {
259 Err(err) => {
260 eprintln!("Failed to load file: `{input_path}`. Error: {err}");
261 std::process::exit(1)
262 }
263 Ok(content) => content,
264 };
265
266 match fe_driver::dump_mir_single_file(&mut db, input_path, &content) {
267 Ok(text) => println!("{text}"),
268 Err(err) => {
269 eprintln!("Unable to dump mir `{input_path}");
270 print_diagnostics(&db, &err.0);
271 std::process::exit(1)
272 }
273 }
274 } else {
275 eprintln!("dumping mir for ingot is not supported yet");
276 std::process::exit(1)
277 }
278}