Skip to content

Commit 2cf3dc6

Browse files
committed
sqlx-macros-core: allow not calling cargo from a proc-macro
Problem: We're trying to build https://github.com/MercuryTechnologies/locally-euclidean with buck2, and we're finding that sqlx is trying to call Cargo to find the workspace root and snoop outside of its build directory. This is trouble for us because we want to be able to cache build actions. Thus, we want to be able to run sqlx in an *extra* offline mode which never calls cargo. Solution: check if all the env-vars are set (which requires that you put nonsense in the database url, but that's ok for us as it ensures we cannot possibly regress any existing users). Problem: If a query is missed in .sqlx/, sqlx calls cargo unconditionally to find the workspace root. Solution: accept SQLX_WORKSPACE_DIR to give the fallback directory. You could make it /var/empty if you want to suppress it altogether. ### Does your PR solve an issue? Not one that either I nor an agent can find. Related Bazel usage: #3555, related env-var shenanigans in the CLI #3963, but neither are solved by this.
1 parent 75bc048 commit 2cf3dc6

3 files changed

Lines changed: 112 additions & 32 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ uuid = "1.12.1"
193193
base64 = { version = "0.22.1", default-features = false, features = ["alloc"] }
194194
cfg-if = "1.0.0"
195195
dotenvy = { version = "0.15.7", default-features = false }
196+
tempfile = "3.10.1"
196197
thiserror = { version = "2.0.18", default-features = false, features = ["std"] }
197198

198199
# Cryptography
@@ -247,7 +248,7 @@ serde = { version = "1.0.219", features = ["derive"] }
247248
serde_json = "1.0.142"
248249
url = "2.2.2"
249250
hex = "0.4.3"
250-
tempfile = "3.10.1"
251+
tempfile = { workspace = true }
251252
criterion = { version = "0.5.1", features = ["async_tokio"] }
252253
libsqlite3-sys = { version = "0.37.0" }
253254

sqlx-macros-core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ syn = { version = "2.0.87", default-features = false, features = ["full", "deriv
8080
quote = { version = "1.0.35", default-features = false }
8181
url = { version = "2.2.2" }
8282

83+
[dev-dependencies]
84+
tempfile = { workspace = true }
85+
8386
[lints.rust.unexpected_cfgs]
8487
level = "warn"
8588
check-cfg = ['cfg(sqlx_macros_unstable)', 'cfg(procmacro2_semver_exempt)']

sqlx-macros-core/src/query/metadata.rs

Lines changed: 107 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,44 @@ impl Metadata {
2929
pub fn workspace_root(&self) -> PathBuf {
3030
let mut root = self.workspace_root.lock().unwrap();
3131
if root.is_none() {
32-
use serde::Deserialize;
33-
use std::process::Command;
34-
35-
let cargo = crate::env("CARGO").unwrap();
36-
37-
let output = Command::new(cargo)
38-
.args(["metadata", "--format-version=1", "--no-deps"])
39-
.current_dir(&self.manifest_dir)
40-
.env_remove("__CARGO_FIX_PLZ")
41-
.output()
42-
.expect("Could not fetch metadata");
43-
44-
#[derive(Deserialize)]
45-
struct CargoMetadata {
46-
workspace_root: PathBuf,
47-
}
48-
49-
let metadata: CargoMetadata =
50-
serde_json::from_slice(&output.stdout).expect("Invalid `cargo metadata` output");
51-
52-
*root = Some(metadata.workspace_root);
32+
*root = Some(resolve_workspace_root(
33+
crate::env("SQLX_WORKSPACE_DIR").ok().map(PathBuf::from),
34+
|| {
35+
use serde::Deserialize;
36+
use std::process::Command;
37+
38+
let cargo = crate::env("CARGO").unwrap();
39+
40+
let output = Command::new(cargo)
41+
.args(["metadata", "--format-version=1", "--no-deps"])
42+
.current_dir(&self.manifest_dir)
43+
.env_remove("__CARGO_FIX_PLZ")
44+
.output()
45+
.expect("Could not fetch metadata");
46+
47+
#[derive(Deserialize)]
48+
struct CargoMetadata {
49+
workspace_root: PathBuf,
50+
}
51+
52+
let metadata: CargoMetadata = serde_json::from_slice(&output.stdout)
53+
.expect("Invalid `cargo metadata` output");
54+
55+
metadata.workspace_root
56+
},
57+
));
5358
}
5459
root.clone().unwrap()
5560
}
5661
}
5762

63+
fn resolve_workspace_root(
64+
override_dir: Option<PathBuf>,
65+
cargo_fallback: impl FnOnce() -> PathBuf,
66+
) -> PathBuf {
67+
override_dir.unwrap_or_else(cargo_fallback)
68+
}
69+
5870
pub fn try_for_crate() -> crate::Result<Arc<Metadata>> {
5971
/// The `MtimeCache` in this type covers the config itself,
6072
/// any changes to which will indirectly invalidate the loaded env vars as well.
@@ -89,7 +101,7 @@ pub fn try_for_crate() -> crate::Result<Arc<Metadata>> {
89101
})
90102
}
91103

92-
fn load_env(
104+
fn load_from_dotenv(
93105
manifest_dir: &Path,
94106
config: &Config,
95107
builder: &mut MtimeCacheBuilder,
@@ -143,20 +155,84 @@ fn load_env(
143155
}
144156
}
145157

158+
Ok(Arc::new(from_dotenv))
159+
}
160+
161+
fn load_env(
162+
manifest_dir: &Path,
163+
config: &Config,
164+
builder: &mut MtimeCacheBuilder,
165+
) -> crate::Result<Arc<MacrosEnv>> {
166+
let database_url_env = crate::env_opt(config.common.database_url_var())?;
167+
let offline_dir_env = crate::env_opt("SQLX_OFFLINE_DIR")?.map(PathBuf::from);
168+
let offline_env = crate::env_opt("SQLX_OFFLINE")?.map(|val| is_truthy_bool(&val));
169+
170+
// Don't load .env files if all environment variables are set: we may be in
171+
// a non-Cargo build system like buck2.
172+
let dotenv = if database_url_env.is_none() || offline_dir_env.is_none() || offline_env.is_none()
173+
{
174+
Some(load_from_dotenv(manifest_dir, config, builder)?)
175+
} else {
176+
None
177+
};
178+
146179
Ok(Arc::new(MacrosEnv {
147-
// Make set variables take precedent
148-
database_url: crate::env_opt(config.common.database_url_var())?
149-
.or(from_dotenv.database_url),
150-
offline_dir: crate::env_opt("SQLX_OFFLINE_DIR")?
151-
.map(PathBuf::from)
152-
.or(from_dotenv.offline_dir),
153-
offline: crate::env_opt("SQLX_OFFLINE")?
154-
.map(|val| is_truthy_bool(&val))
155-
.or(from_dotenv.offline),
180+
// Make set variables take precedence
181+
database_url: database_url_env.or_else(|| dotenv.as_ref()?.database_url.clone()),
182+
offline_dir: offline_dir_env.or_else(|| dotenv.as_ref()?.offline_dir.clone()),
183+
offline: offline_env.or_else(|| dotenv.as_ref()?.offline),
156184
}))
157185
}
158186

159187
/// Returns `true` if `val` is `"true"`,
160188
fn is_truthy_bool(val: &str) -> bool {
161189
val.eq_ignore_ascii_case("true") || val == "1"
162190
}
191+
192+
#[cfg(test)]
193+
mod tests {
194+
use super::*;
195+
196+
#[test]
197+
fn load_from_dotenv_reads_env_file() {
198+
let dir = tempfile::tempdir().unwrap();
199+
std::fs::write(
200+
dir.path().join(".env"),
201+
"DATABASE_URL=postgres://test\nSQLX_OFFLINE_DIR=/some/dir\nSQLX_OFFLINE=true\n",
202+
)
203+
.unwrap();
204+
205+
let cache: MtimeCache<Arc<MacrosEnv>> = MtimeCache::new();
206+
let env = cache
207+
.get_or_try_init(|builder| load_from_dotenv(dir.path(), &Config::default(), builder))
208+
.unwrap();
209+
210+
assert_eq!(env.database_url.as_deref(), Some("postgres://test"));
211+
assert_eq!(env.offline_dir, Some(PathBuf::from("/some/dir")));
212+
assert_eq!(env.offline, Some(true));
213+
}
214+
215+
#[test]
216+
fn load_from_dotenv_empty_when_no_env_file() {
217+
// The ancestor walk finds nothing as long as no ancestor of the OS temp dir has a .env.
218+
let dir = tempfile::tempdir().unwrap();
219+
220+
let cache: MtimeCache<Arc<MacrosEnv>> = MtimeCache::new();
221+
let env = cache
222+
.get_or_try_init(|builder| load_from_dotenv(dir.path(), &Config::default(), builder))
223+
.unwrap();
224+
225+
assert!(env.database_url.is_none());
226+
assert!(env.offline_dir.is_none());
227+
assert!(env.offline.is_none());
228+
}
229+
230+
#[test]
231+
fn resolve_workspace_root_prefers_override() {
232+
let dir = PathBuf::from("/fake/workspace");
233+
let result = resolve_workspace_root(Some(dir.clone()), || {
234+
panic!("cargo fallback must not be called when override is set")
235+
});
236+
assert_eq!(result, dir);
237+
}
238+
}

0 commit comments

Comments
 (0)