1use crate::cli::{Cli, OutputFormat};
4use crate::{buildagent, cache, cli, config, exec, git, i18n, output, remote, tui, version};
5use anyhow::{Context, Result};
6use clap::FromArgMatches;
7use rust_i18n::t;
8use std::io::Write;
9use std::path::PathBuf;
10
11fn pre_detect_lang(raw: &[String]) -> Option<String> {
14 let mut it = raw.iter();
15 while let Some(a) = it.next() {
16 if let Some(v) = a.strip_prefix("--lang=") {
17 return Some(v.to_string());
18 }
19 if a == "--lang" {
20 return it.next().cloned();
21 }
22 }
23 None
24}
25
26pub fn main() {
28 if let Err(e) = run() {
29 eprintln!("{}", t!("error.generic", error = format!("{e:#}")));
30 std::process::exit(1);
31 }
32}
33
34fn run() -> Result<()> {
35 let raw: Vec<String> = std::env::args().collect();
37 i18n::init(pre_detect_lang(&raw).as_deref());
38
39 let matches = cli::localized_command().get_matches();
41 let args = Cli::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
42
43 let level = if args.diag {
45 log::LevelFilter::Trace
46 } else {
47 args.verbosity.to_level()
48 };
49 let mut builder = env_logger::Builder::new();
53 builder.filter_level(level).parse_default_env();
54 match &args.log_file {
55 Some(path) if path.as_os_str().eq_ignore_ascii_case("console") => {
57 builder
58 .format_timestamp(None)
59 .target(env_logger::Target::Stderr);
60 }
61 Some(path) => {
62 let file = std::fs::OpenOptions::new()
63 .create(true)
64 .append(true)
65 .open(path)
66 .with_context(|| t!("error.log_open", path = path.display()))?;
67 builder.target(env_logger::Target::Pipe(Box::new(file)));
69 }
70 None => {
71 builder
72 .format_timestamp(None)
73 .target(env_logger::Target::Stderr);
74 }
75 }
76 builder.init();
77
78 let target = if let Some(url) = &args.url {
80 let opts = remote::DynamicRepoOptions {
81 url: url.clone(),
82 branch: args.branch.clone(),
83 username: args.username.clone(),
84 password: args.password.clone(),
85 commit: args.commit.clone(),
86 location: args.dynamic_repo_location.clone(),
87 };
88 remote::prepare(&opts).with_context(|| t!("error.dynamic_repo").to_string())?
89 } else {
90 args.target_path
91 .clone()
92 .unwrap_or_else(|| args.path.clone())
93 };
94 log::debug!("target path: {}", target.display());
95
96 let repo = git::GitRepo::discover(&target).with_context(|| t!("error.git_open").to_string())?;
97 let work_dir = target.canonicalize().unwrap_or(target);
98 let repo_root: Option<PathBuf> = repo.workdir().map(|p| p.to_path_buf());
99
100 let mut configuration =
101 config::loader::load(args.config.as_deref(), &work_dir, repo_root.as_deref())?;
102 cli::apply_overrides(&mut configuration, &args.override_config);
103
104 if args.show_config {
105 println!("{}", serde_yaml::to_string(&configuration)?);
106 return Ok(());
107 }
108
109 if args.tui {
110 return tui::run(repo, configuration, work_dir);
111 }
112
113 let mut key_inputs = args.override_config.clone();
115 if let Some(b) = &args.branch {
116 key_inputs.push(format!("branch={b}"));
117 }
118 let config_path = args
119 .config
120 .clone()
121 .or_else(|| config::loader::locate(&work_dir, repo_root.as_deref()));
122 let cache_key = if args.nocache {
123 None
124 } else {
125 Some(cache::compute_key(
126 &repo,
127 config_path.as_deref(),
128 &key_inputs,
129 ))
130 };
131
132 let mut variables = match cache_key.as_deref().and_then(|k| cache::load(&repo, k)) {
133 Some(v) => v,
134 None => {
135 let v = version::calculation::calculate(&repo, &configuration, args.branch.clone())
136 .with_context(|| t!("error.calc_failed").to_string())?;
137 if let Some(k) = &cache_key {
138 cache::store(&repo, k, &v);
139 }
140 v
141 }
142 };
143
144 let version_cmd = args
146 .exec_version
147 .clone()
148 .or_else(|| configuration.exec.get("version").cloned());
149 if let Some(cmd) = version_cmd {
150 if let Some(new_ver) = exec::run_version_hook(&cmd, &variables, &work_dir, args.dry_run)? {
151 log::info!("{}", t!("log.version_hook_modified", ver = new_ver));
152 configuration.next_version = Some(new_ver);
153 variables = version::calculation::calculate(&repo, &configuration, args.branch.clone())
154 .with_context(|| t!("error.version_hook_recalc").to_string())?;
155 }
156 }
157
158 if let Some(files) = &args.update_assembly_info {
160 for p in output::files::update_assembly_info(
161 &variables,
162 &work_dir,
163 files,
164 args.ensure_assembly_info,
165 )? {
166 log::info!("{}", t!("log.assemblyinfo_updated", path = p.display()));
167 }
168 }
169 if let Some(files) = &args.update_project_files {
170 for p in output::files::update_project_files(&variables, &work_dir, files)? {
171 log::info!("{}", t!("log.projectfile_updated", path = p.display()));
172 }
173 }
174 if args.update_wix_version_file {
175 let p = output::files::write_wix(&variables, &work_dir)?;
176 log::info!("{}", t!("log.wix_created", path = p.display()));
177 }
178 if let Some(files) = &args.update_package_files {
179 for p in output::files::update_package_files(&variables, &work_dir, files)? {
180 log::info!("{}", t!("log.package_updated", path = p.display()));
181 }
182 }
183
184 if !configuration.exec.is_empty() || args.exec.is_some() {
186 exec::run_hooks(
187 &configuration.exec,
188 args.exec.as_deref(),
189 &variables,
190 &work_dir,
191 args.dry_run,
192 )?;
193 }
194
195 if let Some(name) = &args.show_variable {
197 return emit(&args, output::generator::show_variable(&variables, name)?);
198 }
199 if let Some(template) = &args.format {
200 return emit(
201 &args,
202 output::generator::format_template(&variables, template)?,
203 );
204 }
205
206 let mut rendered = String::new();
208 for (i, fmt) in args.output.iter().enumerate() {
209 if i > 0 {
210 rendered.push('\n');
211 }
212 match fmt {
213 OutputFormat::Json | OutputFormat::File => {
215 rendered.push_str(&output::generator::to_json(&variables)?)
216 }
217 OutputFormat::DotEnv => rendered.push_str(&output::generator::to_dotenv(&variables)),
218 OutputFormat::BuildServer => match buildagent::detect() {
219 Some(agent) => {
220 log::info!("{}", t!("log.agent_detected", name = agent.name()));
221 let ubn = configuration.update_build_number.unwrap_or(true);
222 rendered.push_str(&agent.write_integration(&variables, ubn).join("\n"));
223 }
224 None => rendered.push_str(&output::generator::to_buildserver_env(&variables)),
225 },
226 }
227 }
228 emit(&args, rendered)
229}
230
231fn emit(args: &Cli, content: String) -> Result<()> {
233 if let Some(path) = &args.output_file {
234 let mut f = std::fs::File::create(path)
235 .with_context(|| t!("error.output_file", path = path.display()).to_string())?;
236 f.write_all(content.as_bytes())?;
237 if !content.ends_with('\n') {
238 f.write_all(b"\n")?;
239 }
240 log::info!("{}", t!("log.result_written", path = path.display()));
241 } else {
242 println!("{content}");
243 }
244 Ok(())
245}