fe/task/
build.rs

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    // no file content for ingots
124    ("".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}