1use std::{collections::HashMap, fmt::Display, path::PathBuf, str::FromStr};
4
5#[cfg(feature = "cli")]
6use clap::{
7 error::{ContextKind, ContextValue},
8 ArgAction,
9};
10use url::Url;
11
12pub const SCRATCH_DIR: &str = "/nix/temp-install-dir";
13
14pub const NIX_TARBALL_PATH: &str = env!("NIX_INSTALLER_TARBALL_PATH");
15pub const NIX_TARBALL: &[u8] = include_bytes!(env!("NIX_INSTALLER_TARBALL_PATH"));
19
20#[cfg(feature = "determinate-nix")]
21pub const DETERMINATE_NIXD_BINARY: Option<&[u8]> =
25 Some(include_bytes!(env!("DETERMINATE_NIXD_BINARY_PATH")));
26
27#[cfg(not(feature = "determinate-nix"))]
28pub const DETERMINATE_NIXD_BINARY: Option<&[u8]> = None;
32
33#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq, Eq)]
34#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
35pub enum InitSystem {
36 None,
37 Systemd,
38 Launchd,
39}
40
41impl std::fmt::Display for InitSystem {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 InitSystem::None => write!(f, "none"),
45 InitSystem::Systemd => write!(f, "systemd"),
46 InitSystem::Launchd => write!(f, "launchd"),
47 }
48 }
49}
50
51#[serde_with::serde_as]
57#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
58#[cfg_attr(feature = "cli", derive(clap::Parser))]
59pub struct CommonSettings {
60 #[cfg_attr(
62 feature = "cli",
63 clap(
64 long = "determinate",
65 env = "NIX_INSTALLER_DETERMINATE",
66 default_value = "false"
67 )
68 )]
69 pub determinate_nix: bool,
70
71 #[cfg_attr(
73 feature = "cli",
74 clap(
75 action(ArgAction::SetFalse),
76 default_value = "true",
77 global = true,
78 env = "NIX_INSTALLER_MODIFY_PROFILE",
79 long = "no-modify-profile"
80 )
81 )]
82 pub modify_profile: bool,
83
84 #[cfg_attr(
86 feature = "cli",
87 clap(
88 long,
89 default_value = "nixbld",
90 env = "NIX_INSTALLER_NIX_BUILD_GROUP_NAME",
91 global = true
92 )
93 )]
94 pub nix_build_group_name: String,
95
96 #[cfg_attr(
98 feature = "cli",
99 clap(long, env = "NIX_INSTALLER_NIX_BUILD_GROUP_ID", global = true)
100 )]
101 #[cfg_attr(
102 all(feature = "cli"),
103 clap(default_value_t = default_nix_build_group_id())
104 )]
105 pub nix_build_group_id: u32,
106
107 #[cfg_attr(
109 feature = "cli",
110 clap(long, env = "NIX_INSTALLER_NIX_BUILD_USER_PREFIX", global = true)
111 )]
112 #[cfg_attr(
113 all(target_os = "macos", feature = "cli"),
114 clap(default_value = "_nixbld")
115 )]
116 #[cfg_attr(
117 all(target_os = "linux", feature = "cli"),
118 clap(default_value = "nixbld")
119 )]
120 pub nix_build_user_prefix: String,
121
122 #[cfg_attr(
124 feature = "cli",
125 clap(
126 long,
127 alias = "daemon-user-count",
128 env = "NIX_INSTALLER_NIX_BUILD_USER_COUNT",
129 global = true
130 )
131 )]
132 #[cfg_attr(all(target_os = "macos", feature = "cli"), clap(default_value = "32"))]
133 #[cfg_attr(all(target_os = "linux", feature = "cli"), clap(default_value = "32"))]
134 pub nix_build_user_count: u32,
135
136 #[cfg_attr(
138 feature = "cli",
139 clap(long, env = "NIX_INSTALLER_NIX_BUILD_USER_ID_BASE", global = true)
140 )]
141 #[cfg_attr(
142 all(target_os = "macos", feature = "cli"),
143 doc = "Service users on Mac should be between 200-400"
144 )]
145 #[cfg_attr(
146 all(feature = "cli"),
147 clap(default_value_t = default_nix_build_user_id_base())
148 )]
149 pub nix_build_user_id_base: u32,
150
151 #[cfg_attr(
153 feature = "cli",
154 clap(long, env = "NIX_INSTALLER_NIX_PACKAGE_URL", global = true, value_parser = clap::value_parser!(UrlOrPath), default_value = None)
155 )]
156 pub nix_package_url: Option<UrlOrPath>,
157
158 #[cfg_attr(feature = "cli", clap(long, env = "NIX_INSTALLER_PROXY"))]
160 pub proxy: Option<Url>,
161
162 #[cfg_attr(feature = "cli", clap(long, env = "NIX_INSTALLER_SSL_CERT_FILE"))]
164 pub ssl_cert_file: Option<PathBuf>,
165
166 #[cfg_attr(feature = "cli", clap(long, action = ArgAction::Append, num_args = 0.., env = "NIX_INSTALLER_EXTRA_CONF", global = true))]
168 pub extra_conf: Vec<UrlOrPathOrString>,
169
170 #[cfg_attr(
172 feature = "cli",
173 clap(
174 long,
175 action(ArgAction::SetTrue),
176 default_value = "false",
177 global = true,
178 env = "NIX_INSTALLER_FORCE"
179 )
180 )]
181 pub force: bool,
182
183 #[cfg(feature = "diagnostics")]
184 #[cfg_attr(
186 feature = "cli",
187 clap(
188 long,
189 default_value = None,
190 env = "NIX_INSTALLER_DIAGNOSTIC_ATTRIBUTION",
191 global = true
192 )
193 )]
194 pub diagnostic_attribution: Option<String>,
195
196 #[cfg(feature = "diagnostics")]
197 #[clap(
216 long,
217 env = "NIX_INSTALLER_DIAGNOSTIC_ENDPOINT",
218 global = true,
219 value_parser = crate::diagnostics::diagnostic_endpoint_validator,
220 num_args = 0..=1, default_value = "https://install.determinate.systems/nix/diagnostic"
222 )]
223 pub diagnostic_endpoint: Option<String>,
224}
225
226fn default_nix_build_user_id_base() -> u32 {
227 use target_lexicon::OperatingSystem;
228
229 match OperatingSystem::host() {
230 OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin => 350,
231 _ => 30_000,
232 }
233}
234
235fn default_nix_build_group_id() -> u32 {
236 use target_lexicon::OperatingSystem;
237
238 match OperatingSystem::host() {
239 OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin => 350,
240 _ => 30_000,
241 }
242}
243
244impl CommonSettings {
245 pub async fn default() -> Result<Self, InstallSettingsError> {
247 let nix_build_user_prefix;
248
249 use target_lexicon::{Architecture, OperatingSystem};
250 match (Architecture::host(), OperatingSystem::host()) {
251 (Architecture::X86_64, OperatingSystem::Linux) => {
252 nix_build_user_prefix = "nixbld";
253 },
254 (Architecture::X86_32(_), OperatingSystem::Linux) => {
255 nix_build_user_prefix = "nixbld";
256 },
257 (Architecture::Aarch64(_), OperatingSystem::Linux) => {
258 nix_build_user_prefix = "nixbld";
259 },
260 (Architecture::X86_64, OperatingSystem::MacOSX { .. })
261 | (Architecture::X86_64, OperatingSystem::Darwin) => {
262 nix_build_user_prefix = "_nixbld";
263 },
264 (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. })
265 | (Architecture::Aarch64(_), OperatingSystem::Darwin) => {
266 nix_build_user_prefix = "_nixbld";
267 },
268 _ => {
269 return Err(InstallSettingsError::UnsupportedArchitecture(
270 target_lexicon::HOST,
271 ))
272 },
273 };
274
275 Ok(Self {
276 determinate_nix: false,
277 modify_profile: true,
278 nix_build_group_name: String::from("nixbld"),
279 nix_build_group_id: default_nix_build_group_id(),
280 nix_build_user_id_base: default_nix_build_user_id_base(),
281 nix_build_user_count: 32,
282 nix_build_user_prefix: nix_build_user_prefix.to_string(),
283 nix_package_url: None,
284 proxy: Default::default(),
285 extra_conf: Default::default(),
286 force: false,
287 ssl_cert_file: Default::default(),
288 #[cfg(feature = "diagnostics")]
289 diagnostic_attribution: None,
290 #[cfg(feature = "diagnostics")]
291 diagnostic_endpoint: Some("https://install.determinate.systems/nix/diagnostic".into()),
292 })
293 }
294
295 pub fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
297 let Self {
298 determinate_nix,
299 modify_profile,
300 nix_build_group_name,
301 nix_build_group_id,
302 nix_build_user_prefix,
303 nix_build_user_id_base,
304 nix_build_user_count,
305 nix_package_url,
306 proxy,
307 extra_conf,
308 force,
309 ssl_cert_file,
310 #[cfg(feature = "diagnostics")]
311 diagnostic_attribution: _,
312 #[cfg(feature = "diagnostics")]
313 diagnostic_endpoint,
314 } = self;
315 let mut map = HashMap::default();
316
317 map.insert(
318 "determinate_nix".into(),
319 serde_json::to_value(determinate_nix)?,
320 );
321 map.insert(
322 "modify_profile".into(),
323 serde_json::to_value(modify_profile)?,
324 );
325 map.insert(
326 "nix_build_group_name".into(),
327 serde_json::to_value(nix_build_group_name)?,
328 );
329 map.insert(
330 "nix_build_group_id".into(),
331 serde_json::to_value(nix_build_group_id)?,
332 );
333 map.insert(
334 "nix_build_user_prefix".into(),
335 serde_json::to_value(nix_build_user_prefix)?,
336 );
337 map.insert(
338 "nix_build_user_id_base".into(),
339 serde_json::to_value(nix_build_user_id_base)?,
340 );
341 map.insert(
342 "nix_build_user_count".into(),
343 serde_json::to_value(nix_build_user_count)?,
344 );
345 map.insert(
346 "nix_package_url".into(),
347 serde_json::to_value(nix_package_url)?,
348 );
349 map.insert("proxy".into(), serde_json::to_value(proxy)?);
350 map.insert("ssl_cert_file".into(), serde_json::to_value(ssl_cert_file)?);
351 map.insert("extra_conf".into(), serde_json::to_value(extra_conf)?);
352 map.insert("force".into(), serde_json::to_value(force)?);
353
354 #[cfg(feature = "diagnostics")]
355 map.insert(
356 "diagnostic_endpoint".into(),
357 serde_json::to_value(diagnostic_endpoint)?,
358 );
359
360 Ok(map)
361 }
362}
363
364async fn linux_detect_systemd_started() -> bool {
365 use std::process::Stdio;
366
367 let mut started = false;
368 if std::path::Path::new("/run/systemd/system").exists() {
369 started = tokio::process::Command::new("systemctl")
370 .arg("status")
371 .stdin(Stdio::null())
372 .stdout(Stdio::null())
373 .stderr(Stdio::null())
374 .status()
375 .await
376 .ok()
377 .map(|exit| exit.success())
378 .unwrap_or(false)
379 }
380
381 started
383}
384
385#[serde_with::serde_as]
386#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
387#[cfg_attr(feature = "cli", derive(clap::Parser))]
388pub struct InitSettings {
389 #[cfg_attr(feature = "cli", clap(value_parser, long, env = "NIX_INSTALLER_INIT",))]
391 #[cfg_attr(
392 all(target_os = "macos", feature = "cli"),
393 clap(default_value_t = InitSystem::Launchd)
394 )]
395 #[cfg_attr(
396 all(target_os = "linux", feature = "cli"),
397 clap(default_value_t = InitSystem::Systemd)
398 )]
399 pub init: InitSystem,
400
401 #[cfg_attr(
403 feature = "cli",
404 clap(
405 value_parser,
406 long,
407 action(ArgAction::SetFalse),
408 env = "NIX_INSTALLER_START_DAEMON",
409 default_value_t = true,
410 long = "no-start-daemon"
411 )
412 )]
413 pub start_daemon: bool,
414}
415
416impl InitSettings {
417 pub async fn default() -> Result<Self, InstallSettingsError> {
419 use target_lexicon::{Architecture, OperatingSystem};
420 let (init, start_daemon) = match (Architecture::host(), OperatingSystem::host()) {
421 (Architecture::X86_64, OperatingSystem::Linux) => {
422 (InitSystem::Systemd, linux_detect_systemd_started().await)
423 },
424 (Architecture::X86_32(_), OperatingSystem::Linux) => {
425 (InitSystem::Systemd, linux_detect_systemd_started().await)
426 },
427 (Architecture::Aarch64(_), OperatingSystem::Linux) => {
428 (InitSystem::Systemd, linux_detect_systemd_started().await)
429 },
430 (Architecture::X86_64, OperatingSystem::MacOSX { .. })
431 | (Architecture::X86_64, OperatingSystem::Darwin) => (InitSystem::Launchd, true),
432 (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. })
433 | (Architecture::Aarch64(_), OperatingSystem::Darwin) => (InitSystem::Launchd, true),
434 _ => {
435 return Err(InstallSettingsError::UnsupportedArchitecture(
436 target_lexicon::HOST,
437 ))
438 },
439 };
440
441 Ok(Self { init, start_daemon })
442 }
443
444 pub fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
446 let Self { init, start_daemon } = self;
447 let mut map = HashMap::default();
448
449 map.insert("init".into(), serde_json::to_value(init)?);
450 map.insert("start_daemon".into(), serde_json::to_value(start_daemon)?);
451 Ok(map)
452 }
453
454 pub fn init(&mut self, init: InitSystem) -> &mut Self {
456 self.init = init;
457 self
458 }
459
460 pub fn start_daemon(&mut self, toggle: bool) -> &mut Self {
462 self.start_daemon = toggle;
463 self
464 }
465}
466
467#[non_exhaustive]
469#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
470pub enum InstallSettingsError {
471 #[error("`nix-installer` does not support the `{0}` architecture right now")]
473 UnsupportedArchitecture(target_lexicon::Triple),
474 #[error("Parsing URL")]
476 Parse(
477 #[source]
478 #[from]
479 url::ParseError,
480 ),
481 #[error("JSON serialization or deserialization error")]
483 SerdeJson(
484 #[source]
485 #[from]
486 serde_json::Error,
487 ),
488 #[error("No supported init system found")]
489 InitNotSupported,
490 #[error(transparent)]
491 UrlOrPath(#[from] UrlOrPathError),
492}
493
494#[derive(Debug, thiserror::Error)]
495pub enum UrlOrPathError {
496 #[error("Error parsing URL `{0}`")]
497 Url(String, #[source] url::ParseError),
498 #[error("The specified path `{0}` does not exist")]
499 PathDoesNotExist(PathBuf),
500 #[error("Error fetching URL `{0}`")]
501 Reqwest(Url, #[source] reqwest::Error),
502 #[error("I/O error when accessing `{0}`")]
503 Io(PathBuf, #[source] std::io::Error),
504}
505
506#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, Clone)]
507pub enum UrlOrPath {
508 Url(Url),
509 Path(PathBuf),
510}
511
512impl Display for UrlOrPath {
513 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514 match self {
515 UrlOrPath::Url(url) => f.write_fmt(format_args!("{url}")),
516 UrlOrPath::Path(path) => f.write_fmt(format_args!("{}", path.display())),
517 }
518 }
519}
520
521impl FromStr for UrlOrPath {
522 type Err = UrlOrPathError;
523
524 fn from_str(s: &str) -> Result<Self, Self::Err> {
525 match Url::parse(s) {
526 Ok(url) => Ok(UrlOrPath::Url(url)),
527 Err(url::ParseError::RelativeUrlWithoutBase) => {
528 let path = PathBuf::from(s);
533 if path.exists() {
534 Ok(UrlOrPath::Path(path))
535 } else {
536 Err(UrlOrPathError::PathDoesNotExist(path))
537 }
538 },
539 Err(e) => Err(UrlOrPathError::Url(s.to_string(), e)),
540 }
541 }
542}
543
544#[cfg(feature = "cli")]
545impl clap::builder::TypedValueParser for UrlOrPath {
546 type Value = UrlOrPath;
547
548 fn parse_ref(
549 &self,
550 cmd: &clap::Command,
551 _arg: Option<&clap::Arg>,
552 value: &std::ffi::OsStr,
553 ) -> Result<Self::Value, clap::Error> {
554 let value_str = value.to_str().ok_or_else(|| {
555 let mut err = clap::Error::new(clap::error::ErrorKind::InvalidValue);
556 err.insert(
557 ContextKind::InvalidValue,
558 ContextValue::String(format!("`{value:?}` not a UTF-8 string")),
559 );
560 err
561 })?;
562 match UrlOrPath::from_str(value_str) {
563 Ok(v) => Ok(v),
564 Err(from_str_error) => {
565 let mut err = clap::Error::new(clap::error::ErrorKind::InvalidValue).with_cmd(cmd);
566 err.insert(
567 clap::error::ContextKind::Custom,
568 clap::error::ContextValue::String(from_str_error.to_string()),
569 );
570 Err(err)
571 },
572 }
573 }
574}
575
576#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, Clone)]
577pub enum UrlOrPathOrString {
578 Url(Url),
579 Path(PathBuf),
580 String(String),
581}
582
583impl FromStr for UrlOrPathOrString {
584 type Err = url::ParseError;
585
586 fn from_str(s: &str) -> Result<Self, Self::Err> {
587 match Url::parse(s) {
588 Ok(url) => Ok(UrlOrPathOrString::Url(url)),
589 Err(url::ParseError::RelativeUrlWithoutBase) => {
590 let path = PathBuf::from(s);
595 if path.exists() {
596 Ok(UrlOrPathOrString::Path(path))
597 } else {
598 Ok(UrlOrPathOrString::String(s.into()))
600 }
601 },
602 Err(e) => Err(e),
603 }
604 }
605}
606
607#[cfg(feature = "cli")]
608impl clap::builder::TypedValueParser for UrlOrPathOrString {
609 type Value = UrlOrPathOrString;
610
611 fn parse_ref(
612 &self,
613 cmd: &clap::Command,
614 _arg: Option<&clap::Arg>,
615 value: &std::ffi::OsStr,
616 ) -> Result<Self::Value, clap::Error> {
617 let value_str = value.to_str().ok_or_else(|| {
618 let mut err = clap::Error::new(clap::error::ErrorKind::InvalidValue);
619 err.insert(
620 ContextKind::InvalidValue,
621 ContextValue::String(format!("`{value:?}` not a UTF-8 string")),
622 );
623 err
624 })?;
625 match UrlOrPathOrString::from_str(value_str) {
626 Ok(v) => Ok(v),
627 Err(from_str_error) => {
628 let mut err = clap::Error::new(clap::error::ErrorKind::InvalidValue).with_cmd(cmd);
629 err.insert(
630 clap::error::ContextKind::Custom,
631 clap::error::ContextValue::String(from_str_error.to_string()),
632 );
633 Err(err)
634 },
635 }
636 }
637}
638
639#[cfg(feature = "diagnostics")]
640impl crate::diagnostics::ErrorDiagnostic for InstallSettingsError {
641 fn diagnostic(&self) -> String {
642 let static_str: &'static str = (self).into();
643 static_str.to_string()
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::{FromStr, PathBuf, Url, UrlOrPath, UrlOrPathOrString};
650
651 #[test]
652 fn url_or_path_or_string_parses() -> Result<(), Box<dyn std::error::Error>> {
653 assert_eq!(
654 UrlOrPathOrString::from_str("https://boop.bleat")?,
655 UrlOrPathOrString::Url(Url::from_str("https://boop.bleat")?),
656 );
657 assert_eq!(
658 UrlOrPathOrString::from_str("file:///boop/bleat")?,
659 UrlOrPathOrString::Url(Url::from_str("file:///boop/bleat")?),
660 );
661 assert_eq!(
663 UrlOrPathOrString::from_str(file!())?,
664 UrlOrPathOrString::Path(PathBuf::from_str(file!())?),
665 );
666 assert_eq!(
667 UrlOrPathOrString::from_str("Boop")?,
668 UrlOrPathOrString::String(String::from("Boop")),
669 );
670 Ok(())
671 }
672
673 #[test]
674 fn url_or_path_parses() -> Result<(), Box<dyn std::error::Error>> {
675 assert_eq!(
676 UrlOrPath::from_str("https://boop.bleat")?,
677 UrlOrPath::Url(Url::from_str("https://boop.bleat")?),
678 );
679 assert_eq!(
680 UrlOrPath::from_str("file:///boop/bleat")?,
681 UrlOrPath::Url(Url::from_str("file:///boop/bleat")?),
682 );
683 assert_eq!(
685 UrlOrPath::from_str(file!())?,
686 UrlOrPath::Path(PathBuf::from_str(file!())?),
687 );
688 Ok(())
689 }
690}
691
692pub fn determinate_nix_settings() -> nix_config_parser::NixConfig {
693 let mut cfg = nix_config_parser::NixConfig::new();
694 let settings = cfg.settings_mut();
695
696 settings.insert("netrc-file".into(), "/nix/var/determinate/netrc".into());
697 settings.insert(
698 "extra-substituters".into(),
699 "https://cache.flakehub.com".into(),
700 );
701
702 cfg
703}