use crate::{bds::Bds, brcc::Brcc, consts, dcc::Dcc, msbuild::MsBuild};
use envz::Environment;
use envz::registry::{HKCU, Node, StringEntry};
use inquire::{MultiSelect, Select};
use std::ffi::OsStr;
use std::{
collections::{BTreeSet, HashMap},
fmt::Display,
path::{Path, PathBuf},
str::FromStr,
sync::OnceLock,
};
use strum::{IntoEnumIterator, VariantArray};
#[derive(Debug, strum::EnumString, serde::Serialize)]
pub enum Edition {
#[strum(serialize = "Starter")]
Community,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, strum::EnumString, serde::Serialize)]
pub enum Personality {
#[strum(serialize = "Delphi.Win32")]
Delphi,
#[strum(serialize = "BCB")]
CBuilder,
}
pub type Personalities = BTreeSet<Personality>;
#[derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
strum::EnumIter,
strum::VariantArray,
strum::Display,
clap::ValueEnum,
serde::Serialize,
)]
#[serde(rename_all = "lowercase")]
pub enum Architecture {
#[value(aliases = ["IntelX86", "32bit", "32-bit"])]
#[strum(to_string = "x86")]
X86,
#[value(aliases = ["IntelX64", "64bit", "64-bit"])]
#[strum(to_string = "x64")]
X64,
}
impl Architecture {
fn bin_dir_name(&self) -> &'static str {
match self {
Architecture::X86 => "bin",
Architecture::X64 => "bin64",
}
}
fn reg_name_suffix(&self) -> &'static str {
match self {
Architecture::X86 => "",
Architecture::X64 => " x64",
}
}
pub fn ide_name(&self) -> &'static str {
match self {
Architecture::X86 => "32-bit IDE",
Architecture::X64 => "64-bit IDE",
}
}
pub fn platform(&self) -> Platform {
match self {
Self::X86 => Platform::Win32,
Self::X64 => Platform::Win64,
}
}
}
pub type Architectures = BTreeSet<Architecture>;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, strum::EnumIter, strum::Display)]
pub enum CommandLineTool {
DCC32,
DCC64,
BCC64X,
DCCARM64EC,
DCCOSX64,
DCCOSXARM64,
DCCLINUX64,
DCCAARM,
DCCAARM64,
DCCIOSARM64,
BRCC32,
}
impl CommandLineTool {
pub fn file_name(&self) -> String {
format!("{self:?}.exe")
}
fn path(&self, product_info: &ProductInfo, arch: &Architecture) -> PathBuf {
product_info.bin_dir(arch).join(self.file_name())
}
fn which(&self, product_info: &ProductInfo, arch: &Option<Architecture>) -> Option<PathBuf> {
match arch {
Some(Architecture::X86) => &[Architecture::X86, Architecture::X64],
Some(Architecture::X64) => &[Architecture::X64, Architecture::X86],
None => Architecture::VARIANTS,
}
.iter()
.map(|a| self.path(product_info, a))
.find(|path| path.exists())
}
fn is_dcc(&self) -> bool {
format!("{self:?}").starts_with("DCC")
}
fn which_dcc(
&self,
product_info: &ProductInfo,
arch: &Option<Architecture>,
) -> Option<PathBuf> {
match self.is_dcc() {
true => self.which(product_info, arch),
_ => None,
}
}
pub fn platform(&self) -> Option<Platform> {
Platform::iter().find(|p| &p.command_line_tool() == self)
}
}
pub type CommandLineTools = BTreeSet<CommandLineTool>;
#[derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
strum::EnumIter,
strum::Display,
clap::ValueEnum,
serde::Serialize,
)]
#[value(rename_all = "verbatim")]
pub enum Platform {
Win32,
Win64,
Win64x,
WinARM64EC,
OSX64,
OSXARM64,
Linux64,
Android32,
Android64,
IOSDevice64,
}
impl Platform {
pub fn command_line_tool(&self) -> CommandLineTool {
match self {
Platform::Win32 => CommandLineTool::DCC32,
Platform::Win64 => CommandLineTool::DCC64,
Platform::Win64x => CommandLineTool::BCC64X,
Platform::WinARM64EC => CommandLineTool::DCCARM64EC,
Platform::OSX64 => CommandLineTool::DCCOSX64,
Platform::OSXARM64 => CommandLineTool::DCCOSXARM64,
Platform::Linux64 => CommandLineTool::DCCLINUX64,
Platform::Android32 => CommandLineTool::DCCAARM,
Platform::Android64 => CommandLineTool::DCCAARM64,
Platform::IOSDevice64 => CommandLineTool::DCCIOSARM64,
}
}
}
pub type Platforms = BTreeSet<Platform>;
pub struct ProductInfo {
globals: HashMap<String, String>,
version_info: Option<&'static consts::VersionInfo>,
reg_parent: PathBuf,
version: String,
product_name: String,
personalities: Personalities,
update_number: u32,
supports_command_line_compilation: OnceLock<bool>,
}
#[radstudio_macros::derive_fn_data]
impl ProductInfo {
fn reg_node_with(parent: impl AsRef<Path>, version: impl AsRef<str>) -> envz::Result<Node> {
HKCU.open(parent.as_ref().join(version.as_ref()).display().to_string())
}
fn reg_node(&self) -> envz::Result<Node> {
Self::reg_node_with(&self.reg_parent, self.version())
}
fn new(reg_parent: PathBuf, version: String) -> envz::Result<Self> {
let reg_node = Self::reg_node_with(®_parent, &version)?;
let product_name;
let personalities;
if let Ok(node) = reg_node.open("Personalities") {
product_name = node.get("")?.unwrap_or_default().display().to_string();
personalities = node
.values()?
.filter_map(|(n, _)| Personality::from_str(&n).ok())
.collect()
} else {
product_name = String::new();
personalities = Personalities::new();
};
Ok(Self {
globals: reg_node
.values()?
.map(|(n, v)| (n, v.try_into().unwrap_or_default()))
.collect(),
version_info: consts::VersionInfo::new(&version),
reg_parent,
version,
product_name,
personalities,
update_number: if let Ok(node) = reg_node.open("InstalledUpdates") {
node.get("Main Product Update")?
.unwrap_or_default()
.display()
.to_string()
.split("Update")
.nth(1)
.unwrap_or_default()
.trim()
.parse()
.unwrap_or_default()
} else {
0
},
supports_command_line_compilation: OnceLock::new(),
})
}
pub fn is_known(&self) -> bool {
self.version_info.is_some()
}
pub fn version(&self) -> &str {
&self.version
}
pub fn version_number(&self) -> u32 {
self.version().parse::<f64>().unwrap_or_default() as u32
}
pub fn compiler_version(&self) -> String {
match self.version_number() {
n @ (2..=4 | 6..=12) => format!("{}.0", n + 14),
5 => "18.5".to_string(),
n @ 14..=23 => format!("{}.0", n + 13),
37.. => self.version().to_string(),
_ => "".to_string(),
}
}
pub fn compiler_version_number(&self) -> u32 {
(self.compiler_version().parse::<f64>().unwrap_or_default() * 10.0) as u32
}
pub fn package_version(&self) -> String {
match self.version_number() {
n @ (2..=6 | 14..=23) => (n + 6).to_string(),
n @ 7..=12 => (n + 7).to_string(),
n @ (37..) => n.to_string(),
_ => "".to_string(),
}
}
pub fn package_version_number(&self) -> u32 {
self.package_version().parse::<u32>().unwrap_or_default() * 10
}
pub fn product_family(&self) -> &'static str {
match self.version_number() {
..=3 => "Delphi",
4 => "Borland Developer Studio",
_ => "RAD Studio",
}
}
pub fn product_version(&self) -> String {
match self.version_info {
Some(v) => v.product_version.to_string(),
None => format!("<{}>", self.version()),
}
}
pub fn product_name(&self) -> String {
if self.is_known() || self.product_name.is_empty() {
return format!("{} {}", self.product_family(), self.product_version());
}
self.product_name.clone()
}
pub fn update_number(&self) -> &u32 {
&self.update_number
}
pub fn name(&self) -> String {
match self.update_number() {
0 => self.product_name(),
_ => format!("{}.{}", self.product_name(), self.update_number()),
}
}
pub fn code_name(&self) -> &'static str {
match self.version_info {
Some(v) => v.code_name,
None => "",
}
}
pub fn full_name(&self) -> String {
if self.code_name().is_empty() {
self.name()
} else {
format!("{} {}", self.name(), self.code_name())
}
}
pub fn edition(&self) -> Option<Edition> {
self.globals.get("Edition").and_then(|s| s.parse().ok())
}
pub fn display_name(&self) -> String {
match self.edition() {
Some(e) => format!("{} {e:?} Edition", self.full_name()),
None => self.full_name(),
}
}
pub fn root_dir(&self) -> PathBuf {
self.globals
.get("RootDir")
.map(PathBuf::from)
.unwrap_or_default()
}
pub fn personalities(&self) -> &Personalities {
&self.personalities
}
pub fn architectures(&self) -> Architectures {
Architecture::iter()
.filter_map(|a| self.rsvars_bat(&a).exists().then_some(a))
.collect()
}
pub fn ide_architectures(&self) -> Architectures {
Architecture::iter()
.filter_map(|a| self.bds_exe(&a).exists().then_some(a))
.collect()
}
pub fn platforms(&self) -> Platforms {
Platform::iter()
.filter_map(|p| p.command_line_tool().which(self, &None).map(|_| p))
.collect()
}
pub fn ide_platforms(&self) -> Platforms {
self.ide_architectures()
.iter()
.map(|a| a.platform())
.collect::<BTreeSet<_>>()
}
pub fn supports_command_line_compilation(&self) -> bool {
*self.supports_command_line_compilation.get_or_init(|| {
match CommandLineTool::iter().find_map(|t| t.which_dcc(self, &None)) {
Some(p) => Dcc::new(p).supports_command_line_compilation(),
None => false,
}
})
}
pub fn bin_dir(&self, arch: &Architecture) -> PathBuf {
self.root_dir().join(arch.bin_dir_name())
}
pub fn rsvars_bat(&self, arch: &Architecture) -> PathBuf {
self.bin_dir(arch).join(match arch {
Architecture::X86 => "rsvars.bat",
Architecture::X64 => "rsvars64.bat",
})
}
pub fn bds_exe(&self, arch: &Architecture) -> PathBuf {
self.globals
.get(&format!("App{}", arch.reg_name_suffix()))
.cloned()
.unwrap_or_default()
.into()
}
pub fn command_line_tools(&self, arch: &Architecture) -> CommandLineTools {
CommandLineTool::iter()
.filter_map(|c| c.path(self, arch).exists().then_some(c))
.collect()
}
}
pub struct Installation {
product_info: ProductInfo,
}
impl Installation {
fn new(reg_parent: PathBuf, version: String) -> envz::Result<Self> {
Ok(Self {
product_info: ProductInfo::new(reg_parent, version)?,
})
}
pub fn product_info(&self) -> &ProductInfo {
&self.product_info
}
pub fn msbuild(&self, arch: &Option<Architecture>) -> Option<MsBuild> {
let product_info = self.product_info();
let archs = product_info.architectures();
let arch = match arch.as_ref() {
Some(a) if archs.contains(a) => a,
Some(_) => return None,
None => archs.first()?,
};
Some(MsBuild::new(product_info.rsvars_bat(arch)))
}
pub fn bds(&self, arch: &Option<Architecture>) -> Option<Bds> {
let product_info = self.product_info();
let archs = product_info.ide_architectures();
let arch = match arch.as_ref() {
Some(a) if archs.contains(a) => a,
Some(_) => return None,
None => archs.first()?,
};
Some(Bds::new(product_info.bds_exe(arch)))
}
pub fn dcc(&self, clt: &CommandLineTool, arch: &Option<Architecture>) -> Option<Dcc> {
clt.which_dcc(self.product_info(), arch)
.map(|path| Dcc::new(path))
}
pub fn dcc32(&self, arch: &Option<Architecture>) -> Option<Dcc> {
self.dcc(&CommandLineTool::DCC32, arch)
}
pub fn dcc64(&self, arch: &Option<Architecture>) -> Option<Dcc> {
self.dcc(&CommandLineTool::DCC64, arch)
}
pub fn dccarm64ec(&self, arch: &Option<Architecture>) -> Option<Dcc> {
self.dcc(&CommandLineTool::DCCARM64EC, arch)
}
pub fn brcc32(&self, arch: &Option<Architecture>) -> Option<Brcc> {
CommandLineTool::BRCC32
.which(self.product_info(), arch)
.map(|path| Brcc::new(path))
}
pub fn environment_variables(&self, arch: &Architecture) -> envz::Result<Environment> {
Environment::create(
&self.product_info().reg_node()?,
format!("Environment Variables{}", arch.reg_name_suffix()),
false,
)
}
pub const LIBRARY_PATH: &StringEntry = &StringEntry {
name: "Search Path",
is_expand: false,
};
pub const BROWSING_PATH: &StringEntry = &StringEntry {
name: "Browsing Path",
is_expand: false,
};
pub fn library(&self, platform: &Platform) -> envz::Result<Node> {
self.product_info()
.reg_node()?
.create(format!("Library\\{platform}"))
}
pub fn known_packages(&self, arch: &Architecture) -> envz::Result<Node> {
self.product_info()
.reg_node()?
.create(format!("Known Packages{}", arch.reg_name_suffix()))
}
pub fn register_package(
&self,
arch: &Architecture,
bpl_path: impl AsRef<Path>,
description: impl AsRef<OsStr>,
) -> envz::Result<()> {
self.known_packages(arch)?
.set(bpl_path.as_ref().display().to_string(), description)
}
pub fn unregister_pacakge(
&self,
arch: &Architecture,
bpl_path: impl AsRef<Path>,
) -> envz::Result<()> {
self.known_packages(arch)?
.remove(bpl_path.as_ref().display().to_string())
}
}
impl Display for Installation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.product_info().display_name())
}
}
pub struct Installations {
items: Vec<Installation>,
}
impl std::ops::Deref for Installations {
type Target = Vec<Installation>;
fn deref(&self) -> &Self::Target {
&self.items
}
}
impl Installations {
fn new() -> Self {
Self { items: vec![] }
}
fn push(&mut self, value: Installation) {
self.items.push(value);
}
pub fn find_by_name(&self, name: &str) -> Option<&Installation> {
self.items.iter().find(|i| {
i.product_info.product_name().eq_ignore_ascii_case(name)
|| i.product_info().name().eq_ignore_ascii_case(name)
|| i.product_info().code_name().eq_ignore_ascii_case(name)
|| i.product_info().full_name().eq_ignore_ascii_case(name)
|| i.product_info()
.product_version()
.eq_ignore_ascii_case(name)
|| i.product_info()
.name()
.replace(i.product_info().product_family(), "")
.trim()
.eq_ignore_ascii_case(name)
})
}
pub fn latest(&self) -> Option<&Installation> {
self.items.last()
}
pub fn select(&self, message: impl AsRef<str>, multi: bool) -> Vec<&Installation> {
if self.items.is_empty() {
return vec![];
}
let options: Vec<_> = self.items.iter().collect();
if multi {
match MultiSelect::new(message.as_ref(), options).prompt() {
Ok(v) => v,
_ => vec![],
}
} else {
match Select::new(message.as_ref(), options).prompt() {
Ok(v) => vec![v],
_ => vec![],
}
}
}
}
pub fn find() -> envz::Result<Installations> {
let mut installs = Installations::new();
#[cfg(windows)]
{
for path in [
r"Software\Borland\BDS",
r"Software\CodeGear\BDS",
r"Software\Embarcadero\BDS",
] {
let Ok(bds) = HKCU.open(path) else {
continue;
};
for version in bds.keys()? {
installs.push(Installation::new(PathBuf::from(path), version)?);
}
}
}
installs.items.sort_by(|a, b| {
a.product_info()
.version_number()
.cmp(&b.product_info().version_number())
});
Ok(installs)
}