Skip to content

Commit 7aa0fdb

Browse files
committed
feat(cli): infer tuist cache provider config
1 parent f00f0b7 commit 7aa0fdb

1 file changed

Lines changed: 254 additions & 1 deletion

File tree

crates/once-cli/src/cache_provider.rs

Lines changed: 254 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ fn resolve_config(workspace: &Path, xdg: &Xdg) -> Result<ResolvedCacheProviderCo
7171
.context("loading cache provider")?
7272
{
7373
Some(config) => resolve_provider_config(xdg, config),
74-
None => resolve_default_provider(xdg),
74+
None => {
75+
if let Some(config) = resolve_tuist_workspace_config(workspace) {
76+
Ok(ResolvedCacheProviderConfig::Tuist(config?))
77+
} else {
78+
resolve_default_provider(xdg)
79+
}
80+
}
7581
}
7682
}
7783

@@ -192,6 +198,106 @@ fn default_tuist_config(
192198
}
193199
}
194200

201+
fn resolve_tuist_workspace_config(workspace: &Path) -> Option<Result<TuistCacheConfig>> {
202+
load_tuist_toml_config(workspace)
203+
.or_else(|| load_tuist_swift_config(workspace))
204+
.map(|config| {
205+
let (account, project) = split_full_handle(&config.full_handle).with_context(|| {
206+
format!(
207+
"Tuist project handle `{}` must have the form `account/project`",
208+
config.full_handle
209+
)
210+
})?;
211+
Ok(default_tuist_config(
212+
"tuist".to_string(),
213+
config.url.unwrap_or_else(|| DEFAULT_TUIST_URL.to_string()),
214+
Some(account),
215+
Some(project),
216+
None,
217+
))
218+
})
219+
}
220+
221+
#[derive(Debug, Clone, PartialEq, Eq)]
222+
struct TuistWorkspaceConfig {
223+
full_handle: String,
224+
url: Option<String>,
225+
}
226+
227+
#[derive(Debug, Default, Deserialize)]
228+
#[serde(default)]
229+
struct TuistTomlConfig {
230+
project: Option<String>,
231+
url: Option<String>,
232+
}
233+
234+
fn load_tuist_toml_config(workspace: &Path) -> Option<TuistWorkspaceConfig> {
235+
let path = workspace.join("tuist.toml");
236+
let src = std::fs::read_to_string(path).ok()?;
237+
let config: TuistTomlConfig = toml::from_str(&src).ok()?;
238+
Some(TuistWorkspaceConfig {
239+
full_handle: non_empty(config.project)?,
240+
url: non_empty(config.url),
241+
})
242+
}
243+
244+
fn load_tuist_swift_config(workspace: &Path) -> Option<TuistWorkspaceConfig> {
245+
let path = workspace.join("Tuist.swift");
246+
let src = std::fs::read_to_string(path).ok()?;
247+
Some(TuistWorkspaceConfig {
248+
full_handle: swift_string_argument(&src, "fullHandle")?,
249+
url: swift_string_argument(&src, "url"),
250+
})
251+
}
252+
253+
fn swift_string_argument(src: &str, label: &str) -> Option<String> {
254+
let needle = format!("{label}:");
255+
for line in src.lines() {
256+
let mut rest = line.trim_start();
257+
if rest.starts_with("//") {
258+
continue;
259+
}
260+
while let Some(index) = rest.find(&needle) {
261+
let candidate = &rest[index + needle.len()..];
262+
if let Some(value) = leading_swift_string(candidate) {
263+
return Some(value);
264+
}
265+
rest = candidate.get(1..)?;
266+
}
267+
}
268+
None
269+
}
270+
271+
fn leading_swift_string(src: &str) -> Option<String> {
272+
let trimmed = src.trim_start();
273+
let body = trimmed.strip_prefix('"')?;
274+
let mut value = String::new();
275+
let mut escaped = false;
276+
for ch in body.chars() {
277+
if escaped {
278+
value.push(ch);
279+
escaped = false;
280+
} else if ch == '\\' {
281+
escaped = true;
282+
} else if ch == '"' {
283+
return non_empty(Some(value));
284+
} else {
285+
value.push(ch);
286+
}
287+
}
288+
None
289+
}
290+
291+
fn split_full_handle(full_handle: &str) -> Option<(String, String)> {
292+
let mut parts = full_handle.split('/');
293+
let account = non_empty(parts.next().map(str::to_string))?;
294+
let project = non_empty(parts.next().map(str::to_string))?;
295+
if parts.next().is_some() {
296+
return None;
297+
}
298+
Some((account, project))
299+
}
300+
195301
fn resolve_tuist_oauth_client_id(config_value: Option<String>) -> Option<String> {
196302
non_empty(std::env::var(TUIST_OAUTH_CLIENT_ID_ENV).ok())
197303
.or_else(|| config_value.and_then(|value| non_empty(Some(value))))
@@ -324,6 +430,14 @@ mod tests {
324430
std::fs::write(root.join("once.toml"), body).unwrap();
325431
}
326432

433+
fn write_tuist_swift(root: &Path, body: &str) {
434+
std::fs::write(root.join("Tuist.swift"), body).unwrap();
435+
}
436+
437+
fn write_tuist_toml(root: &Path, body: &str) {
438+
std::fs::write(root.join("tuist.toml"), body).unwrap();
439+
}
440+
327441
fn write_user_config(xdg: &Xdg, body: &str) {
328442
let dir = xdg.config_home.join("once");
329443
std::fs::create_dir_all(&dir).unwrap();
@@ -400,6 +514,145 @@ account = "acme"
400514
assert_eq!(provider, ResolvedCacheProviderConfig::Local);
401515
}
402516

517+
#[test]
518+
fn explicit_local_workspace_beats_tuist_workspace_config() {
519+
let tmp = TempDir::new().unwrap();
520+
let xdg = xdg_under(tmp.path());
521+
write_workspace(
522+
tmp.path(),
523+
r#"
524+
[cache_provider]
525+
kind = "local"
526+
"#,
527+
);
528+
write_tuist_swift(
529+
tmp.path(),
530+
r#"
531+
import ProjectDescription
532+
533+
let tuist = Tuist(
534+
fullHandle: "tuist/app",
535+
url: "https://canary.tuist.dev",
536+
project: .xcode()
537+
)
538+
"#,
539+
);
540+
541+
let provider = resolve_config(tmp.path(), &xdg).unwrap();
542+
assert_eq!(provider, ResolvedCacheProviderConfig::Local);
543+
}
544+
545+
#[test]
546+
fn resolve_uses_tuist_swift_when_workspace_is_unspecified() {
547+
let tmp = TempDir::new().unwrap();
548+
let xdg = xdg_under(tmp.path());
549+
write_tuist_swift(
550+
tmp.path(),
551+
r#"
552+
import ProjectDescription
553+
554+
let tuist = Tuist(
555+
fullHandle: "tuist/app",
556+
url: "https://canary.tuist.dev",
557+
project: .xcode()
558+
)
559+
"#,
560+
);
561+
562+
let config = expect_tuist(resolve_config(tmp.path(), &xdg).unwrap());
563+
assert_eq!(config.url, "https://canary.tuist.dev");
564+
assert_eq!(config.account.as_deref(), Some("tuist"));
565+
assert_eq!(config.project.as_deref(), Some("app"));
566+
assert_eq!(config.provider_name, "tuist");
567+
}
568+
569+
#[test]
570+
fn resolve_uses_tuist_swift_default_url() {
571+
let tmp = TempDir::new().unwrap();
572+
let xdg = xdg_under(tmp.path());
573+
write_tuist_swift(
574+
tmp.path(),
575+
r#"
576+
import ProjectDescription
577+
578+
let tuist = Tuist(
579+
fullHandle: "tuist/app",
580+
project: .xcode()
581+
)
582+
"#,
583+
);
584+
585+
let config = expect_tuist(resolve_config(tmp.path(), &xdg).unwrap());
586+
assert_eq!(config.url, DEFAULT_TUIST_URL);
587+
assert_eq!(config.account.as_deref(), Some("tuist"));
588+
assert_eq!(config.project.as_deref(), Some("app"));
589+
}
590+
591+
#[test]
592+
fn resolve_ignores_commented_tuist_swift_handle() {
593+
let tmp = TempDir::new().unwrap();
594+
let xdg = xdg_under(tmp.path());
595+
write_tuist_swift(
596+
tmp.path(),
597+
r#"
598+
import ProjectDescription
599+
600+
let tuist = Tuist(
601+
// fullHandle: "{account_handle}/{project_handle}",
602+
project: .xcode()
603+
)
604+
"#,
605+
);
606+
607+
let provider = resolve_config(tmp.path(), &xdg).unwrap();
608+
assert_eq!(provider, ResolvedCacheProviderConfig::Local);
609+
}
610+
611+
#[test]
612+
fn resolve_uses_tuist_toml_when_workspace_is_unspecified() {
613+
let tmp = TempDir::new().unwrap();
614+
let xdg = xdg_under(tmp.path());
615+
write_tuist_toml(
616+
tmp.path(),
617+
r#"
618+
project = "tuist/gradle-plugin"
619+
url = "https://canary.tuist.dev"
620+
"#,
621+
);
622+
623+
let config = expect_tuist(resolve_config(tmp.path(), &xdg).unwrap());
624+
assert_eq!(config.url, "https://canary.tuist.dev");
625+
assert_eq!(config.account.as_deref(), Some("tuist"));
626+
assert_eq!(config.project.as_deref(), Some("gradle-plugin"));
627+
assert_eq!(config.provider_name, "tuist");
628+
}
629+
630+
#[test]
631+
fn tuist_toml_beats_tuist_swift_when_both_exist() {
632+
let tmp = TempDir::new().unwrap();
633+
let xdg = xdg_under(tmp.path());
634+
write_tuist_toml(
635+
tmp.path(),
636+
r#"
637+
project = "tuist/toml-app"
638+
"#,
639+
);
640+
write_tuist_swift(
641+
tmp.path(),
642+
r#"
643+
import ProjectDescription
644+
645+
let tuist = Tuist(
646+
fullHandle: "tuist/swift-app",
647+
project: .xcode()
648+
)
649+
"#,
650+
);
651+
652+
let config = expect_tuist(resolve_config(tmp.path(), &xdg).unwrap());
653+
assert_eq!(config.project.as_deref(), Some("toml-app"));
654+
}
655+
403656
#[test]
404657
fn workspace_named_provider_overrides_user_default_scope() {
405658
let tmp = TempDir::new().unwrap();

0 commit comments

Comments
 (0)