@@ -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+
195301fn 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