use cargo_metadata::Package;
use crc::{Crc, CRC_64_ECMA_182};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{fs, io};
mod plist;
struct XcodeTarget {
kind: String,
base_name: String,
cargo_file_name: String,
xcode_product_name: String,
xcode_file_name: String,
compiler_flags: String,
file_type: &'static str,
prod_type: &'static str,
supported_platforms: &'static str,
skip_install: bool,
}
struct XcodeObject {
def: String,
}
struct XcodeObjectTarget<'a> {
id: String,
prod_id: String,
compile_cargo_id: String,
conf_list_id: String,
target: &'a XcodeTarget,
}
struct XcodeSections<'a> {
buildfile: Vec<XcodeObject>,
filereference: Vec<XcodeObject>,
targets: Vec<XcodeObjectTarget<'a>>,
product_ids: Vec<String>,
build_config_section: Vec<XcodeObject>,
build_phase_section: Vec<XcodeObject>,
config_list_section: Vec<XcodeObject>,
}
pub struct Generator {
crc: Crc<u64>,
id_base: u64,
package: Package,
output_dir: Option<PathBuf>,
custom_project_name: Option<String>,
skip_install_everything: bool,
use_rustup_nightly: bool,
default_features: bool,
features: String,
}
struct XcodeTargetKindProps {
cargo_file_name: String,
xcode_file_name: String,
xcode_product_name: String,
file_type: &'static str,
prod_type: &'static str,
supported_platforms: &'static str,
skip_install: bool,
}
const STATIC_LIB_APPLE_PRODUCT_TYPE: &str = "com.apple.product-type.library.static";
const DY_LIB_APPLE_PRODUCT_TYPE: &str = "com.apple.product-type.library.dynamic";
const EXECUTABLE_APPLE_PRODUCT_TYPE: &str = "com.apple.product-type.tool";
impl Generator {
#[must_use]
pub fn new(package: Package, output_dir: Option<PathBuf>, custom_project_name: Option<String>) -> Self {
let crc = Crc::<u64>::new(&CRC_64_ECMA_182);
let id_base = crc.checksum(package.name.as_bytes());
Self {
crc, id_base, package, output_dir, custom_project_name,
skip_install_everything: false,
use_rustup_nightly: false,
default_features: true,
features: String::new(),
}
}
pub fn skip_install(&mut self, in_binaries_too: bool) {
self.skip_install_everything = in_binaries_too;
}
pub fn nightly(&mut self, use_rustup_nightly: bool) {
self.use_rustup_nightly = use_rustup_nightly;
}
pub fn features(&mut self, features: &str, default_features: bool) {
self.features = features.into();
self.default_features = default_features;
}
fn make_id(&self, sort: u8, kind: &str, name: &str) -> String {
let mut crc = self.crc.digest();
crc.update(&self.id_base.to_ne_bytes());
crc.update(kind.as_bytes());
let kind = crc.finalize();
let name = self.crc.checksum(name.as_bytes());
let mut out = format!("CA{:02X}{:08X}{:012X}", sort, kind as u32, name);
out.truncate(24);
out
}
pub fn write_pbxproj(&self) -> Result<PathBuf, io::Error> {
let proj_path = self.prepare_project_path()?;
let proj_data = self.pbxproj()?;
let pbx_path = proj_path.join("project.pbxproj");
let mut f = fs::File::create(pbx_path)?;
f.write_all(proj_data.as_bytes())?;
Ok(proj_path)
}
fn project_targets(&self) -> Vec<XcodeTarget> {
self.package.targets.iter().flat_map(move |target| {
let base_name = self.custom_project_name.as_ref().unwrap_or(&target.name).clone();
let required_features = target.required_features.join(",");
let dylib_exists = target.kind.iter().any(|k| k == "cdylib");
let static_suffix = if dylib_exists { "_static" } else { "" };
target.kind.iter().filter_map(move |kind| {
let p = match kind.as_str() {
"bin" => XcodeTargetKindProps {
cargo_file_name: target.name.clone(),
xcode_file_name: base_name.clone(),
xcode_product_name: base_name.clone(),
file_type: "compiled.mach-o.executable",
prod_type: EXECUTABLE_APPLE_PRODUCT_TYPE,
skip_install: self.skip_install_everything,
supported_platforms: "macosx",
},
"cdylib" => XcodeTargetKindProps {
cargo_file_name: format!("lib{}.dylib", target.name.replace('-', "_")),
xcode_file_name: format!("{base_name}.dylib"),
xcode_product_name: base_name.clone(),
file_type: "compiled.mach-o.dylib",
prod_type: DY_LIB_APPLE_PRODUCT_TYPE,
skip_install: self.skip_install_everything,
supported_platforms: "macosx iphonesimulator iphoneos"
},
"staticlib" => XcodeTargetKindProps {
cargo_file_name: format!("lib{}.a", target.name.replace('-', "_")),
xcode_file_name: format!("lib{base_name}{static_suffix}.a"),
xcode_product_name: format!("{base_name}{static_suffix}"),
file_type: "archive.ar",
prod_type: STATIC_LIB_APPLE_PRODUCT_TYPE,
skip_install: true,
supported_platforms: "xrsimulator xros watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos",
},
_ => return None,
};
let mut compiler_flags = if p.prod_type == EXECUTABLE_APPLE_PRODUCT_TYPE { format!("--bin '{base_name}'") } else { "--lib".into() };
if p.prod_type == EXECUTABLE_APPLE_PRODUCT_TYPE && !required_features.is_empty() {
use std::fmt::Write;
write!(&mut compiler_flags, " --features '{required_features}'").unwrap(); }
if !self.default_features {
compiler_flags += " --no-default-features";
}
Some(XcodeTarget {
kind: kind.to_owned(),
compiler_flags,
supported_platforms: p.supported_platforms,
base_name: base_name.clone(),
cargo_file_name: p.cargo_file_name,
xcode_file_name: p.xcode_file_name,
xcode_product_name: p.xcode_product_name,
file_type: p.file_type,
prod_type: p.prod_type,
skip_install: p.skip_install,
})
})
})
.collect()
}
fn products_pbxproj<'a>(&'a self, cargo_targets: &'a [XcodeTarget], manifest_path_id: &str) -> XcodeSections<'a> {
let mut config_list_section = Vec::new();
let mut build_config_section = Vec::new();
let mut build_phase_section = Vec::new();
let mut targets = Vec::new();
let mut product_ids = Vec::new();
let mut buildfile = Vec::new();
let mut filereference = Vec::new();
for (n, target) in cargo_targets.iter().enumerate() {
let sort = n as u8;
let prod_id = self.make_id(sort, target.file_type, &target.cargo_file_name);
let target_id = self.make_id(sort, target.file_type, &prod_id);
let conf_list_id = self.make_id(sort, "<config-list>", &prod_id);
let conf_release_id = self.make_id(sort, "<config-release>", &prod_id);
let conf_debug_id = self.make_id(sort, "<config-debug>", &prod_id);
let compile_cargo_id = self.make_id(sort, "<cargo>", &prod_id);
let manifest_path_build_object_id = self.make_id(sort, "<cargo-toml>", &prod_id);
targets.push(XcodeObjectTarget {
target,
id: target_id,
prod_id: prod_id.clone(),
compile_cargo_id: compile_cargo_id.clone(),
conf_list_id: conf_list_id.clone(),
});
build_phase_section.push(XcodeObject {
def: format!("\
\t\t{compile_cargo_id} /* Sources */ = {{
\t\t\tisa = PBXSourcesBuildPhase;
\t\t\tbuildActionMask = 2147483647;
\t\t\tfiles = (
\t\t\t\t{manifest_path_build_object_id} /* Cargo.toml in Sources */,
\t\t\t);
\t\t\trunOnlyForDeploymentPostprocessing = 0;
\t\t}};\n"),
});
buildfile.push(XcodeObject {
def: format!("\t\t{manifest_path_build_object_id} /* Cargo.toml in Sources */ = {{isa = PBXBuildFile; fileRef = {manifest_path_id} /* Cargo.toml */; settings = {{COMPILER_FLAGS = \"{compiler_flags}\"; }}; }};\n",
compiler_flags = target.compiler_flags,
),
});
config_list_section.push(XcodeObject {
def: format!("\
\t\t{conf_list_id} /* Build configuration list for PBXNativeTarget \"{base_name}-{kind}\" */ = {{
\t\t\tisa = XCConfigurationList;
\t\t\tbuildConfigurations = (
\t\t\t\t{conf_release_id} /* Release */,
\t\t\t\t{conf_debug_id} /* Debug */,
\t\t\t);
\t\t\tdefaultConfigurationIsVisible = 0;
\t\t\tdefaultConfigurationName = Release;
\t\t}};\n",
base_name = target.base_name,
kind = target.kind,
),
});
let skip_install_flags = if target.skip_install && !self.skip_install_everything {
"INSTALL_GROUP = \"\";
\t\t\t\tINSTALL_MODE_FLAG = \"\";
\t\t\t\tINSTALL_OWNER = \"\";
\t\t\t\tSKIP_INSTALL = YES;"
} else {
""
};
let dylib_flags = if target.prod_type == DY_LIB_APPLE_PRODUCT_TYPE && self.package.version.major != 1 {
format!("DYLIB_COMPATIBILITY_VERSION = {};", self.package.version.major)
} else {
String::new()
};
build_config_section.extend([(conf_release_id, "Release"), (conf_debug_id, "Debug")].iter().map(|(id, name)| XcodeObject {
def: format!("\
\t\t{id} /* {name} */ = {{
\t\t\tisa = XCBuildConfiguration;
\t\t\tbuildSettings = {{
\t\t\t\tCARGO_XCODE_CARGO_DEP_FILE_NAME = {dep_file_name};
\t\t\t\tCARGO_XCODE_CARGO_FILE_NAME = {cargo_file_name};
\t\t\t\t{skip_install_flags}
\t\t\t\tPRODUCT_NAME = {xcode_product_name};
\t\t\t\tSUPPORTED_PLATFORMS = {supported_platforms};
\t\t\t\t{dylib_flags}
\t\t\t}};
\t\t\tname = {name};
\t\t}};\n",
cargo_file_name = plist::quote(&target.cargo_file_name),
dep_file_name = plist::quote(Path::new(&target.cargo_file_name).with_extension("d").file_name().unwrap().to_str().unwrap()),
xcode_product_name = plist::quote(&target.xcode_product_name),
supported_platforms = plist::quote(target.supported_platforms),
),
}));
product_ids.push(format!("{prod_id} /* {} */", target.xcode_file_name));
filereference.push(XcodeObject {
def: format!(
"\t\t{prod_id} /* {xcode_file_name} */ = {{isa = PBXFileReference; explicitFileType = {file_type}; includeInIndex = 0; path = {xcode_file_name_q}; sourceTree = BUILT_PRODUCTS_DIR; }};\n",
xcode_file_name_q = plist::quote(&target.xcode_file_name),
xcode_file_name = target.xcode_file_name,
file_type = plist::quote(target.file_type),
),
});
}
XcodeSections {
buildfile, filereference, targets, product_ids, build_config_section, build_phase_section, config_list_section,
}
}
pub fn pbxproj(&self) -> Result<String, io::Error> {
let main_group_id = self.make_id(0xF0, "", "<root>");
let prod_group_id = self.make_id(0xF1, "", "Products");
let frameworks_group_id = self.make_id(0xF2, "", "Frameworks"); let project_id = self.make_id(0xF3, "", "<project>");
let build_rule_id = self.make_id(0xF4, "", "BuildRule");
let lipo_script_id = self.make_id(0xF5, "", "LipoScript");
let conf_list_id = self.make_id(0xF6, "", "<configuration-list>");
let conf_release_id = self.make_id(0xF7, "configuration", "Release");
let conf_debug_id = self.make_id(0xF8, "configuration", "Debug");
let manifest_path_id = self.make_id(0xF9, "", "Cargo.toml");
let project_name = self.custom_project_name.as_deref().unwrap_or(&self.package.name);
let rust_targets = self.project_targets();
let mut sections = self.products_pbxproj(&rust_targets, &manifest_path_id);
let product_refs = sections.product_ids.iter().map(|id| format!("\t\t\t\t{id},\n")).collect::<String>();
let mut main_folder_refs = Vec::new();
main_folder_refs.push(format!("{manifest_path_id} /* Cargo.toml */"));
let cargo_toml_path = match &self.output_dir {
Some(output_dir) => {
pathdiff::diff_paths(fs::canonicalize(&self.package.manifest_path)?, fs::canonicalize(output_dir)?)
.ok_or_else(|| io::Error::new(io::ErrorKind::Unsupported, format!("warning: Unable to make relative path from {} to {}", self.package.manifest_path, output_dir.display())))?
},
None => "Cargo.toml".into(),
};
if cargo_toml_path.is_absolute() {
eprintln!("warning: Unable to make relative path from {} to {}", self.package.manifest_path, self.output_dir.as_deref().unwrap_or("".as_ref()).display());
}
sections.filereference.push(XcodeObject {
def: format!(
"\t\t{manifest_path_id} /* {cargo_toml_path} */ = {{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = {cargo_toml_path_q}; sourceTree = \"<group>\"; }};\n",
cargo_toml_path = cargo_toml_path.display(),
cargo_toml_path_q = plist::quote(cargo_toml_path.to_str().unwrap()),
),
});
main_folder_refs.push(format!("{prod_group_id} /* Products */"));
main_folder_refs.push(format!("{frameworks_group_id} /* Frameworks */"));
let buildfile = sections.buildfile.into_iter().map(|o| o.def).collect::<String>();
let filereference = sections.filereference.into_iter().map(|o| o.def).collect::<String>();
let build_phase_section = sections.build_phase_section.into_iter().map(|o| o.def).collect::<String>();
let config_list_section = sections.config_list_section.into_iter().map(|o| o.def).collect::<String>();
let build_config_section = sections.build_config_section.into_iter().map(|o| o.def).collect::<String>();
let main_folder_refs = main_folder_refs.iter().map(|id| format!("\t\t\t\t{id},\n")).collect::<String>();
let build_script = include_str!("xcodebuild.sh").replace(" ", " ");
let common_build_settings = |o: &mut plist::Object<'_>, mode: &str| {
o.kv("ADDITIONAL_SDKS", "macosx") .kv("ALWAYS_SEARCH_USER_PATHS", "NO")
.kvq("CARGO_TARGET_DIR", "$(PROJECT_TEMP_DIR)/cargo_target")
.kv("CARGO_XCODE_BUILD_PROFILE", mode)
.kvq("CARGO_XCODE_FEATURES", &self.features)
.kvq("CARGO_XCODE_TARGET_ARCH", "$(CURRENT_ARCH)")
.kv("\"CARGO_XCODE_TARGET_ARCH[arch=arm64]\"", "aarch64")
.kv("\"CARGO_XCODE_TARGET_ARCH[arch=arm64e]\"", "aarch64")
.kv("\"CARGO_XCODE_TARGET_ARCH[arch=i386]\"", "i686")
.kv("\"CARGO_XCODE_TARGET_ARCH[arch=x86_64h]\"", "x86_64") .kvq("CARGO_XCODE_TARGET_OS", "$(PLATFORM_NAME)")
.kv("\"CARGO_XCODE_TARGET_OS[sdk=appletvos]\"", "tvos")
.kv("\"CARGO_XCODE_TARGET_OS[sdk=appletvsimulator]\"", "tvos")
.kv("\"CARGO_XCODE_TARGET_OS[sdk=iphoneos]\"", "ios")
.kvq("\"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator]\"", "ios-sim")
.kv("\"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator][arch=x86_64]\"", "ios")
.kv("\"CARGO_XCODE_TARGET_OS[sdk=macosx*]\"", "darwin")
.kvq("\"CARGO_XCODE_TARGET_OS[sdk=watchsimulator]\"", "watchos-sim")
.kvq("CURRENT_PROJECT_VERSION", format_args!("{major}.{minor}", major = self.package.version.major, minor = self.package.version.minor))
.kvq("MARKETING_VERSION", &self.package.version);
if mode == "debug" {
o.kv("ONLY_ACTIVE_ARCH", "YES");
}
o.kvq("PRODUCT_NAME", &self.package.name)
.kvq("RUSTUP_TOOLCHAIN", if self.use_rustup_nightly { "nightly" } else { "" })
.kv("SDKROOT", "macosx");
if self.skip_install_everything {
o.kv("SKIP_INSTALL", "YES");
}
o.kv("SUPPORTS_MACCATALYST", "YES");
};
let lipo_script = include_str!("lipo.sh");
let crate_version = env!("CARGO_PKG_VERSION");
let mut p = plist::Plist::new();
p.root()
.comment(format_args!("generated with cargo-xcode {crate_version}"))
.kv("archiveVersion", "1")
.obj("classes", None, |_| {})
.kv("objectVersion", "53")
.obj("objects", None, |o| {
o.nl()
.section_comment("Begin PBXBuildFile section")
.raw(buildfile)
.section_comment("End PBXBuildFile section")
.nl()
.section_comment("Begin PBXBuildRule section")
.obj(&build_rule_id, Some("PBXBuildRule"), |p| {
p.kv("isa", "PBXBuildRule")
.kv("compilerSpec", "com.apple.compilers.proxy.script")
.kvq("dependencyFile", "$(DERIVED_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME).d")
.kvq("filePatterns", "*/Cargo.toml") .kv("fileType", "pattern.proxy")
.array("inputFiles", None, |_| {})
.kv("isEditable", "0")
.kvq("name", "Cargo project build")
.array("outputFiles", None, |a| {
a.q("$(OBJECT_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME)");
})
.kvq("script", format_args!("# generated with cargo-xcode {crate_version}\n{build_script}"));
})
.section_comment("End PBXBuildRule section")
.nl()
.section_comment("Begin PBXFileReference section")
.raw(filereference)
.section_comment("End PBXFileReference section")
.nl()
.section_comment("Begin PBXGroup section")
.obj(&main_group_id, None, |f| {
f.kv("isa", "PBXGroup")
.array("children", None, |c| {
c.raw(main_folder_refs);
})
.kvq("sourceTree", "<group>");
})
.obj(&prod_group_id, Some("Products"), |f| {
f.kv("isa", "PBXGroup")
.array("children", None, |c| {
c.raw(product_refs);
})
.kv("name", "Products")
.kvq("sourceTree", "<group>");
})
.obj(&frameworks_group_id, Some("Frameworks"), |f| {
f.kv("isa", "PBXGroup")
.array("children", None, |_| {})
.kv("name", "Frameworks")
.kvq("sourceTree", "<group>");
})
.section_comment("End PBXGroup section")
.nl()
.section_comment("Begin PBXNativeTarget section");
for t in §ions.targets {
let base_kind = format!("{}-{}", t.target.base_name, t.target.kind);
o.obj(&t.id, Some(&base_kind), |d| {
d.kv("isa", "PBXNativeTarget")
.kv("buildConfigurationList", format_args!("{conf_list_id} /* Build configuration list for PBXNativeTarget \"{base_name}-{kind}\" */",
conf_list_id = t.conf_list_id, base_name = t.target.base_name, kind = t.target.kind))
.array("buildPhases", None, |a| {
a.v(format_args!("{} /* Sources */", t.compile_cargo_id))
.v(format_args!("{lipo_script_id} /* Universal Binary lipo */"));
})
.array("buildRules", None, |r| {
r.v(format_args!("{build_rule_id} /* PBXBuildRule */"));
})
.array("dependencies", None, |_| {
})
.kvq("name", &base_kind)
.kvq("productName", &t.target.xcode_file_name)
.kv("productReference", format_args!("{} /* {} */", t.prod_id, t.target.xcode_file_name))
.kvq("productType", t.target.prod_type)
;
});
}
o.section_comment("End PBXNativeTarget section")
.nl()
.section_comment("Begin PBXProject section")
.obj(&project_id, Some("Project object"), |p| {
p.kv("isa", "PBXProject")
.obj("attributes", None, |a| {
a.kv("LastUpgradeCheck", "1500")
.obj("TargetAttributes", None, |t| {
for o in §ions.targets {
t.obj(&o.id, None, |a| {
a.kv("CreatedOnToolsVersion", "9.2")
.kv("ProvisioningStyle", "Automatic");
});
}
});
})
.kv("buildConfigurationList", format_args!("{conf_list_id} /* Build configuration list for PBXProject \"{project_name}\" */"))
.kvq("compatibilityVersion", "Xcode 11.4")
.kv("developmentRegion", "en")
.kv("hasScannedForEncodings", "0")
.array("knownRegions", None, |r| {
r.v("en").v("Base");
})
.kv("mainGroup", main_group_id)
.kv("productRefGroup", format_args!("{prod_group_id} /* Products */"))
.kvq("projectDirPath", "")
.kvq("projectRoot", "")
.array("targets", None, |a| {
for t in §ions.targets {
let base_kind = format!("{}-{}", t.target.base_name, t.target.kind);
a.v(format_args!("{id} /* {base_kind} */", id = t.id));
}
});
})
.section_comment("End PBXProject section")
.nl()
.section_comment("Begin PBXShellScriptBuildPhase section")
.obj(&lipo_script_id, Some("Universal Binary lipo"), |s| {
s.kv("isa", "PBXShellScriptBuildPhase")
.kv("buildActionMask", "2147483647")
.array("files", None, |_| {})
.array("inputFileListPaths", None, |_| {})
.array("inputPaths", None, |p| {
p.q("$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).xcfilelist");
})
.kvq("name", "Universal Binary lipo")
.array("outputFileListPaths", None, |_| {})
.array("outputPaths", None, |o| {
o.q("$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)");
})
.kv("runOnlyForDeploymentPostprocessing", "0")
.kv("shellPath", "/bin/sh")
.kvq("shellScript", format_args!("# generated with cargo-xcode {crate_version}\n{lipo_script}"));
})
.section_comment("End PBXShellScriptBuildPhase section")
.nl()
.section_comment("Begin PBXSourcesBuildPhase section")
.raw(build_phase_section)
.section_comment("End PBXSourcesBuildPhase section")
.nl()
.section_comment("Begin XCBuildConfiguration section")
.raw(build_config_section)
.obj(&conf_release_id, Some("Release"), |c| {
c.kv("isa", "XCBuildConfiguration")
.obj("buildSettings", None, |s| {
common_build_settings(s, "release");
})
.kv("name", "Release");
})
.obj(&conf_debug_id, Some("Debug"), |c| {
c.kv("isa", "XCBuildConfiguration")
.obj("buildSettings", None, |s| {
common_build_settings(s, "debug");
})
.kv("name", "Debug");
})
.section_comment("End XCBuildConfiguration section")
.nl()
.section_comment("Begin XCConfigurationList section")
.raw(config_list_section)
.obj(&conf_list_id, Some(&format!("Build configuration list for PBXProject \"{project_name}\"")), |c| {
c.kv("isa", "XCConfigurationList")
.array("buildConfigurations", None, |c| {
c.v(format_args!("{conf_release_id} /* Release */"))
.v(format_args!("{conf_debug_id} /* Debug */"));
})
.kv("defaultConfigurationIsVisible", "0")
.kv("defaultConfigurationName", "Release");
})
.section_comment("End XCConfigurationList section");
})
.kv("rootObject", format_args!("{project_id} /* Project object */"));
let tpl = p.fin();
Ok(tpl)
}
fn prepare_project_path(&self) -> Result<PathBuf, io::Error> {
let proj_file_name = format!("{}.xcodeproj", self.custom_project_name.as_ref().unwrap_or(&self.package.name));
let proj_path = match &self.output_dir {
Some(path) => path.join(proj_file_name),
None => Path::new(&self.package.manifest_path).with_file_name(proj_file_name),
};
fs::create_dir_all(&proj_path)?;
Ok(proj_path)
}
}