Skip to main content

cargo_xcode/
lib.rs

1//! cargo-xcode is meant to be used from command line. See [CLI usage docs](https://lib.rs/cargo-xcode).
2
3use cargo_metadata::Package;
4use crc::{Crc, CRC_64_ECMA_182};
5use std::borrow::Cow;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::{fs, io};
9
10mod plist;
11
12struct XcodeTarget {
13    name_label: String,
14    cargo_file_name: String,
15    xcode_product_name: String,
16    xcode_file_name: String,
17    compiler_flags: String,
18    file_type: &'static str,
19    prod_type: &'static str,
20    supported_platforms: Cow<'static, str>,
21    skip_install: bool,
22}
23
24struct XcodeTargetIds<'a> {
25    target: &'a XcodeTarget,
26    id: String,
27    compile_cargo_id: String,
28    manifest_path_build_object_id: String,
29    prod_conf_debug_id: String,
30    prod_conf_list_id: String,
31    prod_conf_release_id: String,
32    prod_id: String,
33}
34
35pub struct Generator {
36    crc: Crc<u64>,
37    id_base: u64,
38    package: Package,
39    output_dir: PathBuf,
40    custom_project_name: Option<String>,
41    skip_install_everything: bool,
42    use_rustup_nightly: bool,
43    default_features: bool,
44    features: String,
45    allowed_platforms: Option<Vec<String>>,
46    xcconfigs: Vec<String>,
47}
48
49struct XcodeTargetKindProps {
50    name_label: String,
51    cargo_file_name: String,
52    xcode_file_name: String,
53    xcode_product_name: String,
54    file_type: &'static str,
55    prod_type: &'static str,
56    supported_platforms: Cow<'static, str>,
57    skip_install: bool,
58}
59
60const STATIC_LIB_APPLE_PRODUCT_TYPE: &str = "com.apple.product-type.library.static";
61const DY_LIB_APPLE_PRODUCT_TYPE: &str = "com.apple.product-type.library.dynamic";
62const EXECUTABLE_APPLE_PRODUCT_TYPE: &str = "com.apple.product-type.tool";
63
64impl Generator {
65    pub fn new(package: Package, output_dir: Option<&Path>, custom_project_name: Option<String>) -> io::Result<Self> {
66        let crc = Crc::<u64>::new(&CRC_64_ECMA_182);
67        let id_base = crc.checksum(package.name.as_bytes());
68
69        let output_dir = fs::canonicalize(match output_dir {
70            Some(o) => o,
71            None => package.manifest_path.as_std_path().parent().ok_or(io::ErrorKind::InvalidInput)?,
72        })?;
73
74        Ok(Self {
75            crc, id_base, package, output_dir, custom_project_name,
76            skip_install_everything: false,
77            use_rustup_nightly: false,
78            default_features: true,
79            features: String::new(),
80            allowed_platforms: None,
81            xcconfigs: Vec::new(),
82        })
83    }
84
85    pub fn skip_install(&mut self, in_binaries_too: bool) {
86        self.skip_install_everything = in_binaries_too;
87    }
88
89    pub fn nightly(&mut self, use_rustup_nightly: bool) {
90        self.use_rustup_nightly = use_rustup_nightly;
91    }
92
93    pub fn xcconfigs(&mut self, xcconfigs: Vec<String>) {
94        if xcconfigs.len() > 2 {
95            panic!("Specify maximum of 2 xcconfigs");
96        }
97        self.xcconfigs = xcconfigs;
98    }
99
100    pub fn platforms(&mut self, allowed_platforms: Option<Vec<String>>) {
101        self.allowed_platforms = allowed_platforms;
102    }
103
104    pub fn features(&mut self, features: &str, default_features: bool) {
105        self.features = features.into();
106        self.default_features = default_features;
107    }
108
109    fn make_id(&self, sort: u8, kind: &str, name: &str) -> String {
110        let mut crc = self.crc.digest();
111        crc.update(&self.id_base.to_ne_bytes());
112        crc.update(kind.as_bytes());
113        let kind = crc.finalize();
114
115        let name = self.crc.checksum(name.as_bytes());
116        let mut out = format!("CA{:02X}{:08X}{:012X}", sort, kind as u32, name);
117        out.truncate(24);
118        out
119    }
120
121    pub fn write_pbxproj(&self) -> Result<PathBuf, io::Error> {
122        let proj_path = self.prepare_project_path()?;
123        let proj_data = self.pbxproj()?;
124
125        let pbx_path = proj_path.join("project.pbxproj");
126        let mut f = fs::File::create(pbx_path)?;
127        f.write_all(proj_data.as_bytes())?;
128
129        Ok(proj_path)
130    }
131
132    fn project_targets(&self) -> Vec<XcodeTarget> {
133        self.package.targets.iter().flat_map(move |target| {
134            let base_name = self.custom_project_name.as_ref().unwrap_or(&target.name).clone();
135            let required_features = target.required_features.join(",");
136            let dylib_exists = target.kind.iter().any(|k| k == "cdylib");
137            let static_suffix = if dylib_exists { "_static" } else { "" };
138            target.kind.iter().filter_map(move |kind| {
139                let p = match kind.as_str() {
140                    "bin" => XcodeTargetKindProps {
141                        name_label: format!("{} ({})", target.name, if self.skip_install_everything { "bundled binary" } else { "standalone executable" }),
142                        cargo_file_name: target.name.clone(),
143                        xcode_file_name: base_name.clone(),
144                        xcode_product_name: base_name.clone(),
145                        file_type: "compiled.mach-o.executable",
146                        prod_type: EXECUTABLE_APPLE_PRODUCT_TYPE,
147                        skip_install: self.skip_install_everything,
148                        supported_platforms: self.only_allowed_platforms("macosx"),
149                    },
150                    "cdylib" => XcodeTargetKindProps {
151                        name_label: format!("{}.dylib (cdylib)", target.name),
152                        cargo_file_name: format!("lib{}.dylib", target.name.replace('-', "_")),
153                        xcode_file_name: format!("{base_name}.dylib"),
154                        xcode_product_name: base_name.clone(),
155                        file_type: "compiled.mach-o.dylib",
156                        prod_type: DY_LIB_APPLE_PRODUCT_TYPE,
157                        skip_install: self.skip_install_everything,
158                        supported_platforms: self.only_allowed_platforms("macosx iphonesimulator iphoneos"),
159                    },
160                    "staticlib" => XcodeTargetKindProps {
161                        name_label: format!("{}.a (static library)", target.name),
162                        // must have _static suffix to avoid build errors when dylib also exists
163                        cargo_file_name: format!("lib{}.a", target.name.replace('-', "_")),
164                        xcode_file_name: format!("lib{base_name}{static_suffix}.a"),
165                        xcode_product_name: format!("{base_name}{static_suffix}"),
166                        file_type: "archive.ar",
167                        prod_type: STATIC_LIB_APPLE_PRODUCT_TYPE,
168                        skip_install: true,
169                        supported_platforms: self.only_allowed_platforms("xrsimulator xros watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"),
170                    },
171                    _ => return None,
172                };
173
174                let mut compiler_flags = if p.prod_type == EXECUTABLE_APPLE_PRODUCT_TYPE { format!("--bin '{base_name}'") } else { "--lib".into() };
175                if p.prod_type == EXECUTABLE_APPLE_PRODUCT_TYPE && !required_features.is_empty() {
176                    use std::fmt::Write;
177                    write!(&mut compiler_flags, " --features '{required_features}'").unwrap(); // Xcode escapes \=
178                }
179                if !self.default_features {
180                    compiler_flags += " --no-default-features";
181                }
182
183                Some(XcodeTarget {
184                    name_label: p.name_label,
185                    compiler_flags,
186                    supported_platforms: p.supported_platforms,
187                    cargo_file_name: p.cargo_file_name,
188                    xcode_file_name: p.xcode_file_name,
189                    xcode_product_name: p.xcode_product_name,
190                    file_type: p.file_type,
191                    prod_type: p.prod_type,
192                    skip_install: p.skip_install,
193                })
194            })
195        })
196        .collect()
197    }
198
199    fn target_ids<'a>(&self, cargo_targets: &'a [XcodeTarget]) -> Vec<XcodeTargetIds<'a>> {
200        cargo_targets.iter().enumerate().map(|(n, target)| {
201            let sort = n as u8;
202            let prod_id = self.make_id(sort, target.file_type, &target.cargo_file_name);
203            XcodeTargetIds {
204                target,
205                id: self.make_id(sort, target.file_type, &prod_id),
206                compile_cargo_id: self.make_id(sort, "<cargo>", &prod_id),
207                manifest_path_build_object_id: self.make_id(sort, "<cargo-toml>", &prod_id),
208                prod_conf_debug_id: self.make_id(sort, "<config-debug>", &prod_id),
209                prod_conf_list_id: self.make_id(sort, "<config-list>", &prod_id),
210                prod_conf_release_id: self.make_id(sort, "<config-release>", &prod_id),
211                prod_id,
212            }
213        }).collect()
214    }
215
216    pub fn pbxproj(&self) -> Result<String, io::Error> {
217        let main_group_id = self.make_id(0xF0, "", "<root>");
218        let prod_group_id = self.make_id(0xF1, "", "Products");
219        let frameworks_group_id = self.make_id(0xF2, "", "Frameworks"); // This is a magic name that Xcode uses to show Products
220        let project_id = self.make_id(0xF3, "", "<project>");
221        let build_rule_id = self.make_id(0xF4, "", "BuildRule");
222        let conf_list_id = self.make_id(0xF6, "", "<configuration-list>");
223        let conf_release_id = self.make_id(0xF7, "configuration", "Release");
224        let conf_debug_id = self.make_id(0xF8, "configuration", "Debug");
225        let manifest_path_id = self.make_id(0xF9, "", "Cargo.toml");
226
227        let project_name = self.custom_project_name.as_deref().unwrap_or(&self.package.name);
228        let rust_targets = self.project_targets();
229        let targets = self.target_ids(&rust_targets);
230
231        let mut main_folder_refs = Vec::new();
232
233        main_folder_refs.push(format!("{manifest_path_id} /* Cargo.toml */"));
234
235        let cargo_toml_path = self.relative_path(self.package.manifest_path.as_std_path())?;
236
237        let xcconfigs = self.xcconfigs.iter().enumerate().map(|(n, xc)| {
238            let path = self.relative_path(xc.as_ref())?;
239            let id = self.make_id(0xFD + n as u8, "configuration", xc);
240            Ok((path, id))
241        }).collect::<io::Result<Vec<_>>>()?;
242
243        main_folder_refs.push(format!("{prod_group_id} /* Products */"));
244        main_folder_refs.push(format!("{frameworks_group_id} /* Frameworks */"));
245
246        for (path, id) in &xcconfigs {
247            main_folder_refs.push(format!("{id} /* {name} */", name = path.file_name().unwrap().to_str().unwrap()));
248        }
249
250        let main_folder_refs = main_folder_refs.iter().map(|id| format!("\t\t\t\t{id},\n")).collect::<String>();
251
252        let build_script = include_str!("xcodebuild.sh").replace("  ", " ");
253
254        let supported_platforms_prefixes = rust_targets.iter().flat_map(|t| t.supported_platforms.split(' '))
255            .filter_map(|p| p.as_bytes().first())
256            .fold(vec![], |mut v, prefix| { if !v.contains(prefix) { v.push(*prefix); } v });
257
258        let common_build_settings = |o: &mut plist::Object<'_>, cargo_profile_name: &str| {
259            // build scripts need the macosx toolchain
260            if supported_platforms_prefixes.contains(&b'i') { o.kvq("\"ADDITIONAL_SDKS[sdk=i*]\"", "macosx"); }
261            if supported_platforms_prefixes.contains(&b'w') { o.kvq("\"ADDITIONAL_SDKS[sdk=w*]\"", "macosx"); }
262            if supported_platforms_prefixes.contains(&b'x') { o.kvq("\"ADDITIONAL_SDKS[sdk=x*]\"", "macosx"); }
263            if supported_platforms_prefixes.contains(&b'a') { o.kvq("\"ADDITIONAL_SDKS[sdk=a*]\"", "macosx"); }
264            o.kv("ALWAYS_SEARCH_USER_PATHS", "NO")
265            .kvq("CARGO_TARGET_DIR", "$(PROJECT_TEMP_DIR)/cargo_target")
266            .kvq("CARGO_BUILD_BUILD_DIR", "$(CARGO_TARGET_DIR)")
267            .kv("CARGO_XCODE_BUILD_PROFILE", cargo_profile_name)
268            .kvq("CARGO_XCODE_FEATURES", &self.features)
269            .kvq("CURRENT_PROJECT_VERSION", format_args!("{major}.{minor}", major = self.package.version.major, minor = self.package.version.minor))
270            .kv("ENABLE_USER_SCRIPT_SANDBOXING", "NO") // Cargo touches Cargo.lock
271            .kvq("MARKETING_VERSION", &self.package.version);
272            if cargo_profile_name == "debug" {
273                o.kv("ONLY_ACTIVE_ARCH", "YES");
274            }
275            o.kvq("PRODUCT_NAME", &self.package.name)
276            .kvq("RUSTUP_TOOLCHAIN", if self.use_rustup_nightly { "nightly" } else { "" })
277            .kv("SDKROOT", "macosx");
278            if self.skip_install_everything {
279                o.kv("SKIP_INSTALL", "YES");
280            }
281            o.kv("SUPPORTS_MACCATALYST", "YES");
282        };
283
284        let crate_version = env!("CARGO_PKG_VERSION");
285
286        let mut p = plist::Plist::new();
287        p.root()
288            .comment(format_args!("generated with cargo-xcode {crate_version}"))
289            .kv("archiveVersion", "1")
290            .obj("classes", None, |_| {})
291            .kv("objectVersion", "53")
292            .obj("objects", None, |o| {
293                o.nl()
294                 .section_comment("Begin PBXBuildFile section");
295                 for t in &targets {
296                    o.kv(
297                        format_args!("{} /* Cargo.toml in Sources */", t.manifest_path_build_object_id),
298                        format_args!("{{isa = PBXBuildFile; fileRef = {manifest_path_id} /* Cargo.toml */; settings = {{COMPILER_FLAGS = \"{compiler_flags}\"; }}; }}", compiler_flags = t.target.compiler_flags)
299                    );
300                 }
301                 o.section_comment("End PBXBuildFile section")
302                 .nl()
303                 .section_comment("Begin PBXBuildRule section")
304                 .obj(&build_rule_id, Some("PBXBuildRule"), |p| {
305                    p.kv("isa", "PBXBuildRule")
306                    .kv("compilerSpec", "com.apple.compilers.proxy.script")
307                    .kvq("dependencyFile", "$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).d")
308                    .kvq("filePatterns", "*/Cargo.toml") /* must contain asterisk */
309                    .kv("fileType", "pattern.proxy")
310                    .array("inputFiles", None, |a| {
311                        a.q("$(SRCROOT)/Cargo.lock");
312                        a.q("$(SRCROOT)/src");
313                    })
314                    .kv("isEditable", "0")
315                    .kvq("name", "Cargo project build")
316                    .array("outputFiles", None, |a| {
317                        a.q("$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)");
318                    })
319                    .kv("runOncePerArchitecture", "0")
320                    .kvq("script", format_args!("# generated with cargo-xcode {crate_version}\n{build_script}"));
321                 })
322                 .section_comment("End PBXBuildRule section")
323                 .nl()
324                 .section_comment("Begin PBXFileReference section");
325                 for t in &targets {
326                    // path of product does not seem to work. Xcode writes it, but can't read it.
327                    o.kv(
328                        format_args!("{prod_id} /* {xcode_file_name} */", prod_id = t.prod_id, xcode_file_name = t.target.xcode_file_name),
329                        format_args!("{{isa = PBXFileReference; explicitFileType = {file_type}; includeInIndex = 0; path = {xcode_file_name_q}; sourceTree = BUILT_PRODUCTS_DIR; }}",
330                            xcode_file_name_q = plist::quote(&t.target.xcode_file_name),
331                            file_type = plist::quote(t.target.file_type),
332                        )
333                    );
334                 }
335                 for (xc_path, xc_id) in &xcconfigs {
336                    let xc_filename = xc_path.file_name().unwrap().to_str().unwrap();
337                    o.kv(
338                        format_args!("{xc_id} /* {xc_filename} */"),
339                        format_args!("{{isa = PBXFileReference; lastKnownFileType = text; name = {xc_filename_q}; path = {xc_path_q}; sourceTree = \"<group>\"; }}",
340                            xc_filename_q = plist::quote(xc_filename), xc_path_q = plist::quote(xc_path.to_str().unwrap())),
341                    );
342                 }
343                 o.kv(
344                    format_args!("{manifest_path_id} /* {cargo_toml_path} */", cargo_toml_path = cargo_toml_path.to_str().unwrap()),
345                    format_args!("{{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = {cargo_toml_path_q}; sourceTree = \"<group>\"; }}",
346                        cargo_toml_path_q = plist::quote(cargo_toml_path.to_str().unwrap()),
347                    )
348                 )
349                 .section_comment("End PBXFileReference section")
350                 .nl()
351                 .section_comment("Begin PBXGroup section")
352                 .obj(&main_group_id, None, |f| {
353                    f.kv("isa", "PBXGroup")
354                    .array("children", None, |c| {
355                        c.raw(main_folder_refs);
356                    })
357                    .kvq("sourceTree", "<group>");
358                 })
359                 .obj(&prod_group_id, Some("Products"), |f| {
360                    f.kv("isa", "PBXGroup")
361                    .array("children", None, |c| {
362                        for t in &targets {
363                            c.v(format_args!("{} /* {} */", t.prod_id, t.target.xcode_file_name));
364                        }
365                    })
366                    .kv("name", "Products")
367                    .kvq("sourceTree", "<group>");
368                 })
369                 .obj(&frameworks_group_id, Some("Frameworks"), |f| {
370                    f.kv("isa", "PBXGroup")
371                    .array("children", None, |_| {})
372                    .kv("name", "Frameworks")
373                    .kvq("sourceTree", "<group>");
374                 })
375                 .section_comment("End PBXGroup section")
376                 .nl()
377                 .section_comment("Begin PBXNativeTarget section");
378                 for t in &targets {
379                    o.obj(&t.id, Some(&t.target.name_label), |d| {
380                        d.kv("isa", "PBXNativeTarget")
381                        .kv("buildConfigurationList", format_args!("{conf_list_id} /* Build configuration list for PBXNativeTarget \"{name}\" */",
382                            conf_list_id = t.prod_conf_list_id, name = t.target.name_label))
383                        .array("buildPhases", None, |a| {
384                            a.v(format_args!("{} /* Sources */", t.compile_cargo_id));
385                        })
386                        .array("buildRules", None, |r| {
387                            r.v(format_args!("{build_rule_id} /* PBXBuildRule */"));
388                        })
389                        .array("dependencies", None, |_| {
390                        })
391                        .kvq("name", &t.target.name_label)
392                        .kvq("productName", &t.target.xcode_file_name)
393                        .kv("productReference", format_args!("{} /* {} */", t.prod_id, t.target.xcode_file_name))
394                        .kvq("productType", t.target.prod_type)
395                        ;
396                    });
397                 }
398                 o.section_comment("End PBXNativeTarget section")
399                 .nl()
400                 .section_comment("Begin PBXProject section")
401                 .obj(&project_id, Some("Project object"), |p| {
402                    p.kv("isa", "PBXProject")
403                    .obj("attributes", None, |a| {
404                        a.kv("BuildIndependentTargetsInParallel", "YES")
405                        .kv("LastUpgradeCheck", "1510")
406                        .obj("TargetAttributes", None, |t| {
407                            for o in &targets {
408                                t.obj(&o.id, None, |a| {
409                                    a.kv("CreatedOnToolsVersion", "9.2")
410                                    .kv("ProvisioningStyle", "Automatic");
411                                });
412                            }
413                        });
414                    })
415                    .kv("buildConfigurationList", format_args!("{conf_list_id} /* Build configuration list for PBXProject \"{project_name}\" */"))
416                    .kvq("compatibilityVersion", "Xcode 11.4")
417                    .kv("developmentRegion", "en")
418                    .kv("hasScannedForEncodings", "0")
419                    .array("knownRegions", None, |r| {
420                        r.v("en").v("Base");
421                    })
422                    .kv("mainGroup", main_group_id)
423                    .kv("productRefGroup", format_args!("{prod_group_id} /* Products */"))
424                    .kvq("projectDirPath", "")
425                    .kvq("projectRoot", "")
426                    .array("targets", None, |a| {
427                        for t in &targets {
428                            a.v(format_args!("{id} /* {name} */", id = t.id, name = t.target.name_label));
429                        }
430                    });
431                 })
432                 .section_comment("End PBXProject section")
433                 .nl()
434                 .section_comment("Begin PBXSourcesBuildPhase section");
435                 for t in &targets {
436                    o.obj(&t.compile_cargo_id, Some("Sources"), |b| {
437                        b.kv("isa", "PBXSourcesBuildPhase")
438                        .kv("buildActionMask", "2147483647")
439                        .array("files", None, |f| {
440                            f.v(format_args!("{} /* Cargo.toml in Sources */", t.manifest_path_build_object_id));
441                        })
442                        .kv("runOnlyForDeploymentPostprocessing", "0");
443                    });
444                 }
445                 o.section_comment("End PBXSourcesBuildPhase section")
446                 .nl()
447                 .section_comment("Begin XCBuildConfiguration section");
448                 for t in &targets {
449                    let skip_install_config = t.target.skip_install && !self.skip_install_everything;
450                    for [conf_name, conf_id] in [["Release", &t.prod_conf_release_id], ["Debug", &t.prod_conf_debug_id]] {
451                        o.obj(conf_id, Some(conf_name), |c| {
452                            c.kv("isa", "XCBuildConfiguration")
453                            .obj("buildSettings", None, |b| {
454                                b.kvq("CARGO_XCODE_CARGO_DEP_FILE_NAME", Path::new(&t.target.cargo_file_name).with_extension("d").file_name().unwrap().to_str().unwrap())
455                                .kvq("CARGO_XCODE_CARGO_FILE_NAME", &t.target.cargo_file_name);
456                                if skip_install_config {
457                                    // Xcode tries to chmod it when archiving, even though it doesn't belong to the archive
458                                    b.kvq("INSTALL_GROUP", "")
459                                    .kvq("INSTALL_MODE_FLAG", "")
460                                    .kvq("INSTALL_OWNER", "");
461                                }
462                                 if t.target.prod_type == DY_LIB_APPLE_PRODUCT_TYPE && self.package.version.major != 1 {
463                                     b.kv("DYLIB_COMPATIBILITY_VERSION", self.package.version.major);
464                                 }
465                                b.kvq("PRODUCT_NAME", &t.target.xcode_product_name);
466                                if skip_install_config {
467                                    b.kv("SKIP_INSTALL", "YES");
468                                 }
469                                b.kvq("SUPPORTED_PLATFORMS", &t.target.supported_platforms);
470                            })
471                            .kv("name", conf_name);
472                        });
473                    }
474                 }
475                 o.obj(&conf_release_id, Some("Release"), |c| {
476                    c.kv("isa", "XCBuildConfiguration");
477                    if let Some((xc_path, xc_id)) = xcconfigs.last() {
478                        let xc_filename = xc_path.file_name().unwrap().to_str().unwrap();
479                        c.kv("baseConfigurationReference", format_args!("{xc_id} /* {xc_filename} */"));
480                    }
481
482                    c.obj("buildSettings", None, |s| {
483                        common_build_settings(s, "release");
484                    })
485                    .kv("name", "Release");
486                 })
487                 .obj(&conf_debug_id, Some("Debug"), |c| {
488                    c.kv("isa", "XCBuildConfiguration");
489                    if let Some((xc_path, xc_id)) = xcconfigs.first() {
490                        let xc_filename = xc_path.file_name().unwrap().to_str().unwrap();
491                        c.kv("baseConfigurationReference", format_args!("{xc_id} /* {xc_filename} */"));
492                    }
493
494                    c.obj("buildSettings", None, |s| {
495                        common_build_settings(s, "debug");
496                    })
497                    .kv("name", "Debug");
498                 })
499                 .section_comment("End XCBuildConfiguration section")
500                 .nl()
501                 .section_comment("Begin XCConfigurationList section");
502                 for t in &targets {
503                    o.obj(&t.prod_conf_list_id, Some(&format!("Build configuration list for PBXNativeTarget \"{}\"", t.target.name_label)), |l| {
504                        l.kv("isa", "XCConfigurationList")
505                        .array("buildConfigurations", None, |c| {
506                            c.v(format_args!("{} /* Release */", t.prod_conf_release_id))
507                            .v(format_args!("{} /* Debug */", t.prod_conf_debug_id));
508                        })
509                        .kv("defaultConfigurationIsVisible", "0")
510                        .kv("defaultConfigurationName", "Release");
511                    });
512                 }
513                 o.obj(&conf_list_id, Some(&format!("Build configuration list for PBXProject \"{project_name}\"")), |c| {
514                    c.kv("isa", "XCConfigurationList")
515                    .array("buildConfigurations", None, |c| {
516                        c.v(format_args!("{conf_release_id} /* Release */"))
517                        .v(format_args!("{conf_debug_id} /* Debug */"));
518                    })
519                    .kv("defaultConfigurationIsVisible", "0")
520                    .kv("defaultConfigurationName", "Release");
521                 })
522                 .section_comment("End XCConfigurationList section");
523            })
524            .kv("rootObject", format_args!("{project_id} /* Project object */"));
525        let tpl = p.fin();
526        Ok(tpl)
527    }
528
529    fn relative_path(&self, path: &Path) -> Result<PathBuf, io::Error> {
530        let rel_path = pathdiff::diff_paths(fs::canonicalize(path)?, &self.output_dir)
531            .ok_or_else(|| {
532                io::Error::new(io::ErrorKind::Unsupported, format!("warning: Unable to make relative path from {} to {}", path.display(), self.output_dir.display()))
533            })?;
534
535        if rel_path.is_absolute() {
536            eprintln!("warning: Unable to make relative path from {} to {}", path.display(), self.output_dir.display());
537        }
538        Ok(rel_path)
539    }
540
541    fn prepare_project_path(&self) -> Result<PathBuf, io::Error> {
542        let proj_file_name = format!("{}.xcodeproj", self.custom_project_name.as_ref().unwrap_or(&self.package.name));
543        let proj_path = self.output_dir.join(proj_file_name);
544        fs::create_dir_all(&proj_path)?;
545        Ok(proj_path)
546    }
547
548    fn only_allowed_platforms<'a>(&self, supported_platforms: &'a str) -> Cow<'a, str> {
549        if let Some(a) = &self.allowed_platforms {
550            supported_platforms.split(' ').filter(|&p| a.iter().any(|a| a == p)).collect::<Vec<_>>().join(" ").into()
551        } else {
552            supported_platforms.into()
553        }
554    }
555}