ctplt 0.0.1

A package manager + build system for C and C++
Documentation
mod clang;
mod emscripten;
mod gcc;
mod msvc;
mod nasm;

use std::process;

const CLANG_ID: &str = "clang version ";
const EMSCRIPTEN_ID: &str = "emcc ";
const GCC_ID: &str = "gcc version ";
const NASM_ID: &str = "NASM version ";
const TARGET_PREFIX: &str = "Target: ";

pub trait Assembler {
	fn id(&self) -> String;
	fn version(&self) -> String;

	fn cmd(&self) -> Vec<String>;
	fn out_flag(&self) -> String;
	fn depfile_flags(&self, out_file: &str, dep_file: &str) -> Vec<String>;
}

pub trait Compiler {
	fn id(&self) -> String;
	fn version(&self) -> String;
	fn target(&self) -> String;

	fn cmd(&self) -> Vec<String>;
	fn out_flag(&self) -> String;
	fn depfile_flags(&self, out_file: &str, dep_file: &str) -> Vec<String>;
	fn c_std_flag(&self, std: &str) -> Result<String, String>;
	fn cpp_std_flag(&self, std: &str) -> Result<String, String>;
	fn position_independent_code_flag(&self) -> Option<String>;
	fn position_independent_executable_flag(&self) -> Option<String>;
}

pub trait StaticLinker {
	fn cmd(&self) -> Vec<String>;
}

pub trait ExeLinker {
	fn cmd(&self) -> Vec<String>;
	fn position_independent_executable_flag(&self) -> Option<String>;
}

pub(super) fn identify_assembler(cmd: Vec<String>) -> Result<Box<dyn Assembler>, String> {
	log::debug!("identify_assembler() cmd: {}", cmd.join(" "));
	let exe = match cmd.first() {
		Some(x) => x,
		None => return Err("Assembler command is empty".to_owned()),
	};
	let version_output = match process::Command::new(exe).arg("-v").output() {
		Ok(x) => {
			if !x.status.success() {
				return Err(format!("Assembler command returned non-success exit code: \"{} -v\": {}", exe, x.status));
			}
			String::from_utf8_lossy(&x.stdout).into_owned() + &String::from_utf8_lossy(&x.stderr)
		}
		Err(e) => {
			return Err(format!("Error executing assembler command \"{} -v\": {}", exe, e));
		}
	};
	log::debug!("{} -v output: {}", exe, version_output);

	let lines = version_output.lines().collect::<Vec<&str>>();
	let first_line = match lines.first() {
		None => return Err("Assembler command output empty. Could not identify assembler".to_owned()),
		Some(x) => x,
	};

	if first_line.starts_with(NASM_ID) {
		log::info!("assembler: NASM");
		let version = find_version(first_line, NASM_ID);
		log::info!("assembler version: {}", version);

		return Ok(Box::new(nasm::Nasm { cmd, version }));
	}

	Err(format!("Could not identify assembler \"{}\"", exe))
}

pub(super) fn identify_compiler(cmd: Vec<String>) -> Result<Box<dyn Compiler>, String> {
	log::debug!("identify_compiler() cmd: {}", cmd.join(" "));
	let exe = match cmd.first() {
		Some(x) => x,
		None => return Err("Compiler command is empty".to_owned()),
	};
	// The `-v` flag is a shorthand for '--verbose' or '--version --verbose'
	// and outputs to stderr instead of stdout
	let version_output = match process::Command::new(exe).arg("-v").output() {
		Ok(x) => {
			if !x.status.success() {
				return Err(format!("Compiler command returned non-success exit code: \"{} -v\": {}", exe, x.status));
			}
			String::from_utf8_lossy(&x.stderr).into_owned()
		}
		Err(e) => {
			return Err(format!("Error executing compiler command \"{} -v\": {}", exe, e));
		}
	};
	log::debug!("{} -v output: {}", exe, version_output);

	let lines = version_output.lines().collect::<Vec<&str>>();
	let first_line = match lines.first() {
		None => return Err("Compiler command output empty. Could not identify compiler".to_owned()),
		Some(x) => x,
	};

	if let Some(clang) = identify_clang(first_line, &lines, &cmd)? {
		Ok(clang)
	} else if let Some(gcc) = identify_gcc(&lines, &cmd)? {
		Ok(gcc)
	} else if let Some(emcc) = identify_emscripten(first_line, &lines, &cmd)? {
		Ok(emcc)
	} else {
		Err(format!("Could not identify compiler \"{}\"", exe))
	}
}

pub(super) fn identify_linker(cmd: Vec<String>) -> Result<Box<dyn ExeLinker>, String> {
	log::debug!("identify_linker() cmd: {}", cmd.join(" "));
	let exe = match cmd.first() {
		Some(x) => x,
		None => return Err("Linker command is empty".to_owned()),
	};
	// The `-v` flag is a shorthand for '--verbose' or '--version --verbose'
	// and outputs to stderr instead of stdout
	let version_output = match process::Command::new(exe).arg("-v").output() {
		Ok(x) => {
			if !x.status.success() {
				return Err(format!("Linker command returned non-success exit code: \"{} -v\": {}", exe, x.status));
			}
			String::from_utf8_lossy(&x.stderr).into_owned()
		}
		Err(e) => {
			return Err(format!("Error executing linker command \"{} -v\": {}", exe, e));
		}
	};
	log::debug!("{} -v output: {}", exe, version_output);

	let lines = version_output.lines().collect::<Vec<&str>>();
	let first_line = match lines.first() {
		None => return Err("Linker command output empty. Could not identify linker".to_owned()),
		Some(x) => x,
	};

	if let Some(clang) = identify_clang(first_line, &lines, &cmd)? {
		Ok(clang)
	} else if let Some(gcc) = identify_gcc(&lines, &cmd)? {
		Ok(gcc)
	} else if let Some(emcc) = identify_emscripten(first_line, &lines, &cmd)? {
		Ok(emcc)
	} else {
		Err(format!("Could not identify linker \"{}\"", exe))
	}
}

fn identify_clang(first_line: &str, lines: &[&str], cmd: &[String]) -> Result<Option<Box<clang::Clang>>, String> {
	if !first_line.starts_with(CLANG_ID) && !first_line.contains(&(String::from(" ") + CLANG_ID)) {
		return Ok(None);
	}
	log::info!("compiler: clang");
	let version = find_version(first_line, CLANG_ID);
	log::info!("compiler version: {}", version);

	let target = match lines.iter().find(|l| l.starts_with(TARGET_PREFIX)) {
		None => return Err(format!("Could not find \"{}\" in compiler output", TARGET_PREFIX)),
		Some(x) => x[TARGET_PREFIX.len()..].to_owned(),
	};
	log::info!("compiler target: {}", target);

	let target_windows = target.contains("-windows-");
	Ok(Some(Box::new(clang::Clang { cmd: cmd.to_vec(), version, target, target_windows })))
}

fn identify_gcc(lines: &[&str], cmd: &[String]) -> Result<Option<Box<gcc::Gcc>>, String> {
	if let Some(line) = lines.iter().find(|l| l.starts_with(GCC_ID)) {
		log::info!("compiler: gcc");

		let version = find_version(line, GCC_ID);
		log::info!("compiler version: {}", version);

		let target = match lines.iter().find(|l| l.starts_with(TARGET_PREFIX)) {
			None => return Err(format!("Could not find \"{}\" in compiler output", TARGET_PREFIX)),
			Some(x) => x[TARGET_PREFIX.len()..].to_owned(),
		};
		log::info!("compiler target: {}", target);

		Ok(Some(Box::new(gcc::Gcc { cmd: cmd.to_vec(), version, target })))
	} else {
		Ok(None)
	}
}

fn identify_emscripten(
	first_line: &str,
	lines: &[&str],
	cmd: &[String],
) -> Result<Option<Box<emscripten::Emscripten>>, String> {
	if !first_line.starts_with(EMSCRIPTEN_ID) {
		return Ok(None);
	}
	log::info!("compiler: emscripten");

	let close_paren_idx = first_line
		.chars()
		.position(|x| x == ')')
		.map_or(EMSCRIPTEN_ID.len(), |x| x + 1);
	let bgn_idx = close_paren_idx
		+ first_line[close_paren_idx..]
			.chars()
			.position(|x| !x.is_whitespace())
			.unwrap_or(0);
	let version = match first_line[bgn_idx..].find(' ') {
		None => &first_line[bgn_idx..],
		Some(offset) => &first_line[bgn_idx..bgn_idx + offset],
	}
	.to_owned();
	log::info!("compiler version: {}", version);

	let target = match lines.iter().find(|l| l.starts_with(TARGET_PREFIX)) {
		None => return Err(format!("Could not find \"{}\" in compiler output", TARGET_PREFIX)),
		Some(x) => x[TARGET_PREFIX.len()..].to_owned(),
	};
	log::info!("compiler target: {}", target);

	Ok(Some(Box::new(emscripten::Emscripten { cmd: cmd.to_vec(), version, target })))
}

fn find_version(line: &str, ver_str: &str) -> String {
	let bgn_idx = line.find(ver_str).unwrap() + ver_str.len();
	let version = match line[bgn_idx..].find(' ') {
		None => &line[bgn_idx..],
		Some(offset) => &line[bgn_idx..bgn_idx + offset],
	};
	version.to_owned()
}

pub(super) fn msvc_compiler() -> Box<dyn Compiler> {
	Box::new(msvc::Msvc {})
}

// # Expected outputs

// ## clang on Ubuntu
// Ubuntu clang version 17.0.0 (++20230911073219+0176e8729ea4-1~exp1~20230911073329.40)
// Target: x86_64-pc-linux-gnu
// Thread model: posix
// InstalledDir: /usr/bin
// Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/11
// Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/12
// Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/12
// Candidate multilib: .;@m64
// Selected multilib: .;@m64
// Found CUDA installation: /usr/local/cuda, version

// ## clang on Windows
// clang version 16.0.1
// Target: x86_64-pc-windows-msvc
// Thread model: posix
// InstalledDir: C:\Program Files\LLVM\bin

// ## gcc on Ubuntu
// Using built-in specs.
// COLLECT_GCC=g++
// COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper
// OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
// OFFLOAD_TARGET_DEFAULT=1
// Target: x86_64-linux-gnu
// Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.4.0-1ubuntu1~22.04' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2
// Thread model: posix
// Supported LTO compression algorithms: zlib zstd
// gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)