tauri/tooling/cli/src/info.rs

943 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
helpers::{config::get as get_config, framework::infer_from_package_json as infer_framework},
interface::rust::get_workspace_dir,
Result,
};
use clap::Parser;
use colored::Colorize;
use serde::Deserialize;
use std::{
collections::HashMap,
fmt::Write,
fs::{read_dir, read_to_string},
panic,
path::{Path, PathBuf},
process::Command,
};
#[derive(Deserialize)]
struct YarnVersionInfo {
data: Vec<String>,
}
#[derive(Clone, Deserialize)]
struct CargoLockPackage {
name: String,
version: String,
source: Option<String>,
}
#[derive(Deserialize)]
struct CargoLock {
package: Vec<CargoLockPackage>,
}
#[derive(Deserialize)]
struct JsCliVersionMetadata {
version: String,
node: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct VersionMetadata {
#[serde(rename = "cli.js")]
js_cli: JsCliVersionMetadata,
}
#[derive(Clone, Deserialize)]
struct CargoManifestDependencyPackage {
version: Option<String>,
git: Option<String>,
branch: Option<String>,
rev: Option<String>,
path: Option<PathBuf>,
}
#[derive(Clone, Deserialize)]
#[serde(untagged)]
enum CargoManifestDependency {
Version(String),
Package(CargoManifestDependencyPackage),
}
#[derive(Deserialize)]
struct CargoManifestPackage {
version: String,
}
#[derive(Deserialize)]
struct CargoManifest {
package: CargoManifestPackage,
dependencies: HashMap<String, CargoManifestDependency>,
}
#[derive(Debug, PartialEq, Eq)]
enum PackageManager {
Npm,
Pnpm,
Yarn,
Berry,
}
#[derive(Debug, Parser)]
#[clap(about = "Shows information about Tauri dependencies and project configuration")]
pub struct Options;
fn version_metadata() -> Result<VersionMetadata> {
serde_json::from_str::<VersionMetadata>(include_str!("../metadata.json")).map_err(Into::into)
}
#[cfg(not(debug_assertions))]
pub(crate) fn cli_current_version() -> Result<String> {
version_metadata().map(|meta| meta.js_cli.version)
}
#[cfg(not(debug_assertions))]
pub(crate) fn cli_upstream_version() -> Result<String> {
let upstream_metadata = match ureq::get(
"https://raw.githubusercontent.com/tauri-apps/tauri/dev/tooling/cli/metadata.json",
)
.timeout(std::time::Duration::from_secs(3))
.call()
{
Ok(r) => r,
Err(ureq::Error::Status(code, _response)) => {
let message = format!("Unable to find updates at the moment. Code: {}", code);
return Err(anyhow::Error::msg(message));
}
Err(ureq::Error::Transport(transport)) => {
let message = format!(
"Unable to find updates at the moment. Error: {:?}",
transport.kind()
);
return Err(anyhow::Error::msg(message));
}
};
upstream_metadata
.into_string()
.and_then(|meta_str| Ok(serde_json::from_str::<VersionMetadata>(&meta_str)))
.and_then(|json| Ok(json.unwrap().js_cli.version))
.map_err(|e| anyhow::Error::new(e))
}
fn crate_latest_version(name: &str) -> Option<String> {
let url = format!("https://docs.rs/crate/{}/", name);
match ureq::get(&url).call() {
Ok(response) => match (response.status(), response.header("location")) {
(302, Some(location)) => Some(location.replace(&url, "")),
_ => None,
},
Err(_) => None,
}
}
#[allow(clippy::let_and_return)]
fn cross_command(bin: &str) -> Command {
#[cfg(target_os = "windows")]
let cmd = {
let mut cmd = Command::new("cmd");
cmd.arg("/c").arg(bin);
cmd
};
#[cfg(not(target_os = "windows"))]
let cmd = Command::new(bin);
cmd
}
fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Option<String>> {
match pm {
PackageManager::Yarn => {
let mut cmd = cross_command("yarn");
let output = cmd
.arg("info")
.arg(name)
.args(&["version", "--json"])
.output()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let info: YarnVersionInfo = serde_json::from_str(&stdout)?;
Ok(Some(info.data.last().unwrap().to_string()))
} else {
Ok(None)
}
}
PackageManager::Berry => {
let mut cmd = cross_command("yarn");
let output = cmd
.arg("npm")
.arg("info")
.arg(name)
.args(["--fields", "version", "--json"])
.output()?;
if output.status.success() {
let info: crate::PackageJson =
serde_json::from_reader(std::io::Cursor::new(output.stdout)).unwrap();
Ok(info.version)
} else {
Ok(None)
}
}
PackageManager::Npm => {
let mut cmd = cross_command("npm");
let output = cmd.arg("show").arg(name).arg("version").output()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(Some(stdout.replace('\n', "")))
} else {
Ok(None)
}
}
PackageManager::Pnpm => {
let mut cmd = cross_command("pnpm");
let output = cmd.arg("info").arg(name).arg("version").output()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(Some(stdout.replace('\n', "")))
} else {
Ok(None)
}
}
}
}
fn npm_package_version<P: AsRef<Path>>(
pm: &PackageManager,
name: &str,
app_dir: P,
) -> crate::Result<Option<String>> {
let (output, regex) = match pm {
PackageManager::Yarn => (
cross_command("yarn")
.args(&["list", "--pattern"])
.arg(name)
.args(&["--depth", "0"])
.current_dir(app_dir)
.output()?,
None,
),
PackageManager::Berry => (
cross_command("yarn")
.arg("info")
.arg(name)
.current_dir(app_dir)
.output()?,
Some(regex::Regex::new("Version: ([\\da-zA-Z\\-\\.]+)").unwrap()),
),
PackageManager::Npm => (
cross_command("npm")
.arg("list")
.arg(name)
.args(&["version", "--depth", "0"])
.current_dir(app_dir)
.output()?,
None,
),
PackageManager::Pnpm => (
cross_command("pnpm")
.arg("list")
.arg(name)
.args(&["--parseable", "--depth", "0"])
.current_dir(app_dir)
.output()?,
None,
),
};
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let regex = regex.unwrap_or_else(|| regex::Regex::new("@([\\da-zA-Z\\-\\.]+)").unwrap());
Ok(
regex
.captures_iter(&stdout)
.last()
.and_then(|cap| cap.get(1).map(|v| v.as_str().to_string())),
)
} else {
Ok(None)
}
}
fn get_version(command: &str, args: &[&str]) -> crate::Result<Option<String>> {
let output = cross_command(command)
.args(args)
.arg("--version")
.output()?;
let version = if output.status.success() {
Some(
String::from_utf8_lossy(&output.stdout)
.replace('\n', "")
.replace('\r', ""),
)
} else {
None
};
Ok(version)
}
#[cfg(windows)]
fn webview2_version() -> crate::Result<Option<String>> {
let powershell_path = std::env::var("SYSTEMROOT").map_or_else(
|_| "powershell.exe".to_string(),
|p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
);
// check 64bit machine-wide installation
let output = Command::new(&powershell_path)
.args(&["-NoProfile", "-Command"])
.arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
.output()?;
if output.status.success() {
return Ok(Some(
String::from_utf8_lossy(&output.stdout).replace('\n', ""),
));
}
// check 32bit machine-wide installation
let output = Command::new(&powershell_path)
.args(&["-NoProfile", "-Command"])
.arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
.output()?;
if output.status.success() {
return Ok(Some(
String::from_utf8_lossy(&output.stdout).replace('\n', ""),
));
}
// check user-wide installation
let output = Command::new(&powershell_path)
.args(&["-NoProfile", "-Command"])
.arg("Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
.output()?;
if output.status.success() {
return Ok(Some(
String::from_utf8_lossy(&output.stdout).replace('\n', ""),
));
}
Ok(None)
}
#[cfg(windows)]
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct VsInstanceInfo {
display_name: String,
}
#[cfg(windows)]
const VSWHERE: &[u8] = include_bytes!("../scripts/vswhere.exe");
#[cfg(windows)]
fn build_tools_version() -> crate::Result<Option<Vec<String>>> {
let mut vswhere = std::env::temp_dir();
vswhere.push("vswhere.exe");
if !vswhere.exists() {
if let Ok(mut file) = std::fs::File::create(&vswhere) {
use std::io::Write;
let _ = file.write_all(VSWHERE);
}
}
let output = cross_command(vswhere.to_str().unwrap())
.args(&[
"-prerelease",
"-products",
"*",
"-requiresAny",
"-requires",
"Microsoft.VisualStudio.Workload.NativeDesktop",
"-requires",
"Microsoft.VisualStudio.Workload.VCTools",
"-format",
"json",
])
.output()?;
Ok(if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let instances: Vec<VsInstanceInfo> = serde_json::from_str(&stdout)?;
Some(
instances
.iter()
.map(|i| i.display_name.clone())
.collect::<Vec<String>>(),
)
} else {
None
})
}
fn active_rust_toolchain() -> crate::Result<Option<String>> {
let output = cross_command("rustup")
.args(["show", "active-toolchain"])
.output()?;
let toolchain = if output.status.success() {
Some(
String::from_utf8_lossy(&output.stdout)
.replace('\n', "")
.replace('\r', "")
.split('(')
.collect::<Vec<&str>>()[0]
.into(),
)
} else {
None
};
Ok(toolchain)
}
fn crate_version(
tauri_dir: &Path,
manifest: Option<&CargoManifest>,
lock: Option<&CargoLock>,
name: &str,
) -> (String, Option<String>) {
let crate_lock_packages: Vec<CargoLockPackage> = lock
.as_ref()
.map(|lock| {
lock
.package
.iter()
.filter(|p| p.name == name)
.cloned()
.collect()
})
.unwrap_or_default();
let (crate_version_string, found_crate_versions) =
match (&manifest, &lock, crate_lock_packages.len()) {
(Some(_manifest), Some(_lock), 1) => {
let crate_lock_package = crate_lock_packages.first().unwrap();
let version_string = if let Some(s) = &crate_lock_package.source {
if s.starts_with("git") {
format!("{} ({})", s, crate_lock_package.version)
} else {
crate_lock_package.version.clone()
}
} else {
crate_lock_package.version.clone()
};
(version_string, vec![crate_lock_package.version.clone()])
}
(None, Some(_lock), 1) => {
let crate_lock_package = crate_lock_packages.first().unwrap();
let version_string = if let Some(s) = &crate_lock_package.source {
if s.starts_with("git") {
format!("{} ({})", s, crate_lock_package.version)
} else {
crate_lock_package.version.clone()
}
} else {
crate_lock_package.version.clone()
};
(
format!("{} (no manifest)", version_string),
vec![crate_lock_package.version.clone()],
)
}
_ => {
let mut found_crate_versions = Vec::new();
let mut is_git = false;
let manifest_version = match manifest.and_then(|m| m.dependencies.get(name).cloned()) {
Some(tauri) => match tauri {
CargoManifestDependency::Version(v) => {
found_crate_versions.push(v.clone());
v
}
CargoManifestDependency::Package(p) => {
if let Some(v) = p.version {
found_crate_versions.push(v.clone());
v
} else if let Some(p) = p.path {
let manifest_path = tauri_dir.join(&p).join("Cargo.toml");
let v = match read_to_string(&manifest_path)
.map_err(|_| ())
.and_then(|m| toml::from_str::<CargoManifest>(&m).map_err(|_| ()))
{
Ok(manifest) => manifest.package.version,
Err(_) => "unknown version".to_string(),
};
format!("path:{:?} [{}]", p, v)
} else if let Some(g) = p.git {
is_git = true;
let mut v = format!("git:{}", g);
if let Some(branch) = p.branch {
let _ = write!(v, "&branch={}", branch);
} else if let Some(rev) = p.rev {
let _ = write!(v, "#{}", rev);
}
v
} else {
"unknown manifest".to_string()
}
}
},
None => "no manifest".to_string(),
};
let lock_version = match (lock, crate_lock_packages.is_empty()) {
(Some(_lock), false) => crate_lock_packages
.iter()
.map(|p| p.version.clone())
.collect::<Vec<String>>()
.join(", "),
(Some(_lock), true) => "unknown lockfile".to_string(),
_ => "no lockfile".to_string(),
};
(
format!(
"{} {}({})",
manifest_version,
if is_git { "(git manifest)" } else { "" },
lock_version
),
found_crate_versions,
)
}
};
let crate_version = found_crate_versions
.into_iter()
.map(|v| semver::Version::parse(&v).unwrap())
.max();
let suffix = match (crate_version, crate_latest_version(name)) {
(Some(version), Some(target_version)) => {
let target_version = semver::Version::parse(&target_version).unwrap();
if version < target_version {
Some(format!(" (outdated, latest: {})", target_version))
} else {
None
}
}
_ => None,
};
(crate_version_string, suffix)
}
fn indent(spaces: usize) {
print!("{}", " ".repeat(spaces));
}
struct Section(&'static str);
impl Section {
fn display(&self) {
println!();
println!("{}", self.0.yellow().bold());
}
}
struct VersionBlock {
name: String,
version: String,
target_version: String,
indentation: usize,
}
impl VersionBlock {
fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
target_version: "".into(),
indentation: 2,
}
}
fn target_version(mut self, version: impl Into<String>) -> Self {
self.target_version = version.into();
self
}
fn display(&self) {
indent(self.indentation);
print!("{} ", "".cyan());
print!("{}", self.name.bold());
print!(": ");
print!(
"{}",
if self.version.is_empty() {
"Not installed!".red().to_string()
} else {
self.version.clone()
}
);
if !(self.version.is_empty() || self.target_version.is_empty()) {
let version = semver::Version::parse(self.version.as_str()).unwrap();
let target_version = semver::Version::parse(self.target_version.as_str()).unwrap();
if version < target_version {
print!(
" ({}, latest: {})",
"outdated".red(),
self.target_version.green()
);
}
}
println!();
}
}
struct InfoBlock {
key: String,
value: String,
indentation: usize,
}
impl InfoBlock {
fn new(key: impl Into<String>, val: impl Into<String>) -> Self {
Self {
key: key.into(),
value: val.into(),
indentation: 2,
}
}
fn display(&self) {
indent(self.indentation);
print!("{} ", "".cyan());
print!("{}", self.key.bold());
print!(": ");
print!("{}", self.value.clone());
println!();
}
}
pub fn command(_options: Options) -> Result<()> {
Section("Environment").display();
let os_info = os_info::get();
VersionBlock::new(
"OS",
format!(
"{} {} {:?}",
os_info.os_type(),
os_info.version(),
os_info.bitness()
),
)
.display();
#[cfg(windows)]
VersionBlock::new(
"Webview2",
webview2_version().unwrap_or_default().unwrap_or_default(),
)
.display();
#[cfg(windows)]
{
let build_tools = build_tools_version()
.unwrap_or_default()
.unwrap_or_default();
if build_tools.is_empty() {
InfoBlock::new("MSVC", "").display();
} else {
InfoBlock::new("MSVC", "").display();
for i in build_tools {
indent(6);
println!("{}", format!("{} {}", "-".cyan(), i));
}
}
}
let hook = panic::take_hook();
panic::set_hook(Box::new(|_info| {
// do nothing
}));
let app_dir = panic::catch_unwind(crate::helpers::app_paths::app_dir)
.map(Some)
.unwrap_or_default();
panic::set_hook(hook);
let yarn_version = get_version("yarn", &[])
.unwrap_or_default()
.unwrap_or_default();
let metadata = version_metadata()?;
VersionBlock::new(
"Node.js",
get_version("node", &[])
.unwrap_or_default()
.unwrap_or_default()
.chars()
.skip(1)
.collect::<String>(),
)
.target_version(metadata.js_cli.node.replace(">= ", ""))
.display();
VersionBlock::new(
"npm",
get_version("npm", &[])
.unwrap_or_default()
.unwrap_or_default(),
)
.display();
VersionBlock::new(
"pnpm",
get_version("pnpm", &[])
.unwrap_or_default()
.unwrap_or_default(),
)
.display();
VersionBlock::new("yarn", &yarn_version).display();
VersionBlock::new(
"rustup",
get_version("rustup", &[])
.unwrap_or_default()
.map(|v| {
let mut s = v.split(' ');
s.next();
s.next().unwrap().to_string()
})
.unwrap_or_default(),
)
.display();
VersionBlock::new(
"rustc",
get_version("rustc", &[])
.unwrap_or_default()
.map(|v| {
let mut s = v.split(' ');
s.next();
s.next().unwrap().to_string()
})
.unwrap_or_default(),
)
.display();
VersionBlock::new(
"cargo",
get_version("cargo", &[])
.unwrap_or_default()
.map(|v| {
let mut s = v.split(' ');
s.next();
s.next().unwrap().to_string()
})
.unwrap_or_default(),
)
.display();
InfoBlock::new(
"Rust toolchain",
active_rust_toolchain()
.unwrap_or_default()
.unwrap_or_default(),
)
.display();
Section("Packages").display();
let mut package_manager = PackageManager::Npm;
if let Some(app_dir) = &app_dir {
let app_dir_entries = read_dir(app_dir)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect::<Vec<String>>();
package_manager = get_package_manager(&app_dir_entries)?;
}
if package_manager == PackageManager::Yarn
&& yarn_version
.chars()
.next()
.map(|c| c > '1')
.unwrap_or_default()
{
package_manager = PackageManager::Berry;
}
VersionBlock::new(
format!("{} {}", "@tauri-apps/cli", "[NPM]".dimmed()),
metadata.js_cli.version,
)
.target_version(
npm_latest_version(&package_manager, "@tauri-apps/cli")
.unwrap_or_default()
.unwrap_or_default(),
)
.display();
if let Some(app_dir) = &app_dir {
VersionBlock::new(
format!("{} {}", "@tauri-apps/api", "[NPM]".dimmed()),
npm_package_version(&package_manager, "@tauri-apps/api", app_dir)
.unwrap_or_default()
.unwrap_or_default(),
)
.target_version(
npm_latest_version(&package_manager, "@tauri-apps/api")
.unwrap_or_default()
.unwrap_or_default(),
)
.display();
}
let hook = panic::take_hook();
panic::set_hook(Box::new(|_info| {
// do nothing
}));
let tauri_dir = panic::catch_unwind(crate::helpers::app_paths::tauri_dir)
.map(Some)
.unwrap_or_default();
panic::set_hook(hook);
if tauri_dir.is_some() || app_dir.is_some() {
if let Some(tauri_dir) = tauri_dir.clone() {
let manifest: Option<CargoManifest> =
if let Ok(manifest_contents) = read_to_string(tauri_dir.join("Cargo.toml")) {
toml::from_str(&manifest_contents).ok()
} else {
None
};
let lock: Option<CargoLock> = get_workspace_dir()
.ok()
.and_then(|p| read_to_string(p.join("Cargo.lock")).ok())
.and_then(|s| toml::from_str(&s).ok());
for (dep, label) in [
("tauri", format!("{} {}", "tauri", "[RUST]".dimmed())),
(
"tauri-build",
format!("{} {}", "tauri-build", "[RUST]".dimmed()),
),
("tao", format!("{} {}", "tao", "[RUST]".dimmed())),
("wry", format!("{} {}", "wry", "[RUST]".dimmed())),
] {
let (version_string, version_suffix) =
crate_version(&tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
VersionBlock::new(
label,
format!(
"{},{}",
version_string,
version_suffix.unwrap_or_else(|| "".into())
),
)
.display();
}
}
}
if tauri_dir.is_some() || app_dir.is_some() {
Section("App").display();
if tauri_dir.is_some() {
if let Ok(config) = get_config(None) {
let config_guard = config.lock().unwrap();
let config = config_guard.as_ref().unwrap();
InfoBlock::new(
"build-type",
if config.tauri.bundle.active {
"bundle".to_string()
} else {
"build".to_string()
},
)
.display();
InfoBlock::new(
"CSP",
config
.tauri
.security
.csp
.clone()
.map(|c| c.to_string())
.unwrap_or_else(|| "unset".to_string()),
)
.display();
InfoBlock::new("distDir", config.build.dist_dir.to_string()).display();
InfoBlock::new("devPath", config.build.dev_path.to_string()).display();
}
}
if let Some(app_dir) = app_dir {
if let Ok(package_json) = read_to_string(app_dir.join("package.json")) {
let (framework, bundler) = infer_framework(&package_json);
if let Some(framework) = framework {
InfoBlock::new("framework", framework.to_string()).display();
}
if let Some(bundler) = bundler {
InfoBlock::new("bundler", bundler.to_string()).display();
}
} else {
println!("package.json not found");
}
}
}
if let Some(app_dir) = app_dir {
Section("App directory structure").display();
let dirs = read_dir(app_dir)?
.filter(|p| p.is_ok() && p.as_ref().unwrap().path().is_dir())
.collect::<Vec<Result<std::fs::DirEntry, _>>>();
let dirs_len = dirs.len();
for (i, entry) in dirs.into_iter().enumerate() {
let entry = entry?;
let prefix = if i + 1 == dirs_len {
"└─".cyan()
} else {
"├─".cyan()
};
println!(
" {} {}",
prefix,
entry.path().file_name().unwrap().to_string_lossy()
);
}
}
Ok(())
}
fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> crate::Result<PackageManager> {
let mut use_npm = false;
let mut use_pnpm = false;
let mut use_yarn = false;
for name in app_dir_entries {
if name.as_ref() == "package-lock.json" {
use_npm = true;
} else if name.as_ref() == "pnpm-lock.yaml" {
use_pnpm = true;
} else if name.as_ref() == "yarn.lock" {
use_yarn = true;
}
}
if !use_npm && !use_pnpm && !use_yarn {
println!("WARNING: no lock files found, defaulting to npm");
return Ok(PackageManager::Npm);
}
let mut found = Vec::new();
if use_npm {
found.push("npm");
}
if use_pnpm {
found.push("pnpm");
}
if use_yarn {
found.push("yarn");
}
if found.len() > 1 {
return Err(anyhow::anyhow!(
"only one package mangager should be used, but found {}\nplease remove unused package manager lock files",
found.join(" and ")
));
}
if use_npm {
Ok(PackageManager::Npm)
} else if use_pnpm {
Ok(PackageManager::Pnpm)
} else {
Ok(PackageManager::Yarn)
}
}