|
| 1 | +#[cfg(procmacro2_semver_exempt)] |
| 2 | +use std::collections::HashSet; |
1 | 3 | use std::collections::{hash_map, HashMap};
|
| 4 | +#[cfg(procmacro2_semver_exempt)] |
| 5 | +use std::hash::{BuildHasherDefault, DefaultHasher}; |
2 | 6 | use std::path::{Path, PathBuf};
|
3 | 7 | use std::sync::{Arc, LazyLock, Mutex};
|
4 | 8 | use std::{fs, io};
|
@@ -115,20 +119,19 @@ static METADATA: LazyLock<Mutex<HashMap<String, Metadata>>> = LazyLock::new(Defa
|
115 | 119 | // reflect the workspace dir: https://github.com/rust-lang/cargo/issues/3946
|
116 | 120 | fn init_metadata(manifest_dir: &String) -> crate::Result<Metadata> {
|
117 | 121 | let manifest_dir: PathBuf = manifest_dir.into();
|
| 122 | + let config = Config::try_from_crate_or_default()?; |
118 | 123 |
|
119 |
| - let (database_url, offline, offline_dir) = load_dot_env(&manifest_dir); |
| 124 | + load_env(&manifest_dir, &config); |
120 | 125 |
|
121 | 126 | let offline = env("SQLX_OFFLINE")
|
122 |
| - .ok() |
123 |
| - .or(offline) |
124 | 127 | .map(|s| s.eq_ignore_ascii_case("true") || s == "1")
|
125 | 128 | .unwrap_or(false);
|
126 | 129 |
|
127 |
| - let offline_dir = env("SQLX_OFFLINE_DIR").ok().or(offline_dir); |
| 130 | + let offline_dir = env("SQLX_OFFLINE_DIR").ok(); |
128 | 131 |
|
129 |
| - let config = Config::try_from_crate_or_default()?; |
130 |
| - |
131 |
| - let database_url = env(config.common.database_url_var()).ok().or(database_url); |
| 132 | + let database_url = env(config.common.database_url_var()) |
| 133 | + .ok() |
| 134 | + .or_else(|| env("DATABASE_URL").ok()); |
132 | 135 |
|
133 | 136 | Ok(Metadata {
|
134 | 137 | manifest_dir,
|
@@ -415,64 +418,96 @@ where
|
415 | 418 | Ok(ret_tokens)
|
416 | 419 | }
|
417 | 420 |
|
| 421 | +#[cfg(procmacro2_semver_exempt)] |
| 422 | +static TRACKED_ENV_VARS: Mutex<HashSet<String, BuildHasherDefault<DefaultHasher>>> = |
| 423 | + Mutex::new(HashSet::with_hasher(BuildHasherDefault::new())); |
| 424 | + |
418 | 425 | /// Get the value of an environment variable, telling the compiler about it if applicable.
|
419 | 426 | fn env(name: &str) -> Result<String, std::env::VarError> {
|
420 | 427 | #[cfg(procmacro2_semver_exempt)]
|
421 |
| - { |
| 428 | + if TRACKED_ENV_VARS.lock().unwrap().insert(name.to_string()) { |
| 429 | + // Avoid tracking the same env var multiple times, which would undesirably modify |
| 430 | + // build system state and thus behavior in case we change var values. |
422 | 431 | proc_macro::tracked_env::var(name)
|
| 432 | + } else { |
| 433 | + std::env::var(name) |
423 | 434 | }
|
424 | 435 |
|
425 | 436 | #[cfg(not(procmacro2_semver_exempt))]
|
426 |
| - { |
427 |
| - std::env::var(name) |
428 |
| - } |
| 437 | + std::env::var(name) |
429 | 438 | }
|
430 | 439 |
|
431 |
| -/// Get `DATABASE_URL`, `SQLX_OFFLINE` and `SQLX_OFFLINE_DIR` from the `.env`. |
432 |
| -fn load_dot_env(manifest_dir: &Path) -> (Option<String>, Option<String>, Option<String>) { |
433 |
| - let mut env_path = manifest_dir.join(".env"); |
434 |
| - |
435 |
| - // If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this, |
436 |
| - // otherwise fallback to default dotenv file. |
437 |
| - #[cfg_attr(not(procmacro2_semver_exempt), allow(unused_variables))] |
438 |
| - let env_file = if env_path.exists() { |
439 |
| - let res = dotenvy::from_path_iter(&env_path); |
440 |
| - match res { |
441 |
| - Ok(iter) => Some(iter), |
442 |
| - Err(e) => panic!("failed to load environment from {env_path:?}, {e}"), |
443 |
| - } |
444 |
| - } else { |
445 |
| - #[allow(unused_assignments)] |
446 |
| - { |
447 |
| - env_path = PathBuf::from(".env"); |
| 440 | +/// Load configuration environment variables from a `.env` file, without overriding existing |
| 441 | +/// environment variables. If applicable, the compiler is informed about the loaded env vars |
| 442 | +/// and the `.env` files they may come from. |
| 443 | +fn load_env(manifest_dir: &Path, config: &Config) { |
| 444 | + let loadable_vars = [ |
| 445 | + "DATABASE_URL", |
| 446 | + "SQLX_OFFLINE", |
| 447 | + "SQLX_OFFLINE_DIR", |
| 448 | + config.common.database_url_var(), |
| 449 | + ]; |
| 450 | + |
| 451 | + let (found_dotenv, candidate_dotenv_paths) = find_dotenv(&manifest_dir); |
| 452 | + |
| 453 | + // Tell the compiler to watch the candidate `.env` paths for changes. It's important to |
| 454 | + // watch them all, because there are several possible locations where a `.env` file |
| 455 | + // might be read, and we want to react to changes in any of them. |
| 456 | + #[cfg(procmacro2_semver_exempt)] |
| 457 | + for path in &candidate_dotenv_paths { |
| 458 | + if let Some(path) = path.to_str() { |
| 459 | + proc_macro::tracked_path::path(path); |
448 | 460 | }
|
449 |
| - dotenvy::dotenv_iter().ok() |
450 |
| - }; |
451 |
| - |
452 |
| - let mut offline = None; |
453 |
| - let mut database_url = None; |
454 |
| - let mut offline_dir = None; |
| 461 | + } |
455 | 462 |
|
456 |
| - if let Some(env_file) = env_file { |
457 |
| - // tell the compiler to watch the `.env` for changes. |
458 |
| - #[cfg(procmacro2_semver_exempt)] |
459 |
| - if let Some(env_path) = env_path.to_str() { |
460 |
| - proc_macro::tracked_path::path(env_path); |
| 463 | + // Tell the compiler about the environment variables we care about before we load them |
| 464 | + // from any `.env`, so the build system can react to changes in their original values, |
| 465 | + // not the values we load from a potential `.env` file tracked above, which should not |
| 466 | + // take precedence. |
| 467 | + #[cfg(procmacro2_semver_exempt)] |
| 468 | + for name in &loadable_vars { |
| 469 | + if TRACKED_ENV_VARS.lock().unwrap().insert(name.to_string()) { |
| 470 | + proc_macro::tracked_env::var(name); |
461 | 471 | }
|
| 472 | + } |
462 | 473 |
|
463 |
| - for item in env_file { |
464 |
| - let Ok((key, value)) = item else { |
| 474 | + if let Some(dotenv_path) = found_dotenv |
| 475 | + .then_some(candidate_dotenv_paths) |
| 476 | + .iter() |
| 477 | + .flatten() |
| 478 | + .last() |
| 479 | + { |
| 480 | + for dotenv_var_result in dotenvy::from_path_iter(dotenv_path) |
| 481 | + .ok() |
| 482 | + .into_iter() |
| 483 | + .flatten() |
| 484 | + { |
| 485 | + let Ok((key, value)) = dotenv_var_result else { |
465 | 486 | continue;
|
466 | 487 | };
|
467 | 488 |
|
468 |
| - match key.as_str() { |
469 |
| - "DATABASE_URL" => database_url = Some(value), |
470 |
| - "SQLX_OFFLINE" => offline = Some(value), |
471 |
| - "SQLX_OFFLINE_DIR" => offline_dir = Some(value), |
472 |
| - _ => {} |
473 |
| - }; |
| 489 | + if loadable_vars.contains(&&*key) && std::env::var(&key).is_err() { |
| 490 | + std::env::set_var(key, value); |
| 491 | + } |
474 | 492 | }
|
475 | 493 | }
|
| 494 | +} |
| 495 | + |
| 496 | +fn find_dotenv(mut dir: &Path) -> (bool, Vec<PathBuf>) { |
| 497 | + let mut candidate_files = vec![]; |
| 498 | + |
| 499 | + loop { |
| 500 | + candidate_files.push(dir.join(".env")); |
| 501 | + let candidate_file = candidate_files.last().unwrap(); |
476 | 502 |
|
477 |
| - (database_url, offline, offline_dir) |
| 503 | + if candidate_file.is_file() { |
| 504 | + return (true, candidate_files); |
| 505 | + } |
| 506 | + |
| 507 | + if let Some(parent) = dir.parent() { |
| 508 | + dir = parent; |
| 509 | + } else { |
| 510 | + return (false, candidate_files); |
| 511 | + } |
| 512 | + } |
478 | 513 | }
|
0 commit comments