tauri/tooling/cli.rs/src/info.rs

596 lines
16 KiB
Rust
Raw Normal View History

feat(license): SPDX Headers (#1449) * chore(licenses): api Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(licenses): scripts Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): cli/core Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): cli/tauri-bundler Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): workflows Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): require license_template in rust Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): core/tauri Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): core/tauri-api Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): core/tauri-build Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): core/tauri-codegen Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): core/tauri-macros Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): core/tauri-updater Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): core/tauri-utils Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): examples Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): cli/tauri.js Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): changefile Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): place both licenses in root Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): package.json SPDX Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): SPDX everywhere Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * fix(tauri.js): tests more time for ubuntu Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): commons conservancy language Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): add spdx file Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * fix(license): clippy Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com> * chore(license): language Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com>
2021-04-11 01:09:09 +03:00
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::helpers::{
app_paths::{app_dir, tauri_dir},
config::get as get_config,
};
use serde::Deserialize;
use std::{
collections::HashMap,
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,
}
#[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>,
path: Option<PathBuf>,
#[serde(default)]
features: Vec<String>,
}
#[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(Default)]
pub struct Info;
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,
}
}
fn npm_latest_version(use_yarn: bool, name: &str) -> crate::Result<Option<String>> {
2021-04-22 23:34:46 +03:00
let mut cmd;
if use_yarn {
#[cfg(target_os = "windows")]
{
cmd = Command::new("cmd");
cmd.arg("/c").arg("yarn");
}
#[cfg(not(target_os = "windows"))]
{
cmd = Command::new("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)
}
} else {
#[cfg(target_os = "windows")]
{
cmd = Command::new("cmd");
cmd.arg("/c").arg("npm");
}
#[cfg(not(target_os = "windows"))]
{
cmd = Command::new("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)
}
}
}
fn npm_package_version<P: AsRef<Path>>(
use_yarn: bool,
name: &str,
app_dir: P,
) -> crate::Result<Option<String>> {
2021-04-22 23:34:46 +03:00
let mut cmd;
let output = if use_yarn {
#[cfg(target_os = "windows")]
{
cmd = Command::new("cmd");
cmd.arg("/c").arg("yarn");
}
#[cfg(not(target_os = "windows"))]
{
cmd = Command::new("yarn")
}
cmd
.args(&["list", "--pattern"])
.arg(name)
.args(&["--depth", "0"])
.current_dir(app_dir)
.output()?
} else {
#[cfg(target_os = "windows")]
{
cmd = Command::new("cmd");
cmd.arg("/c").arg("npm");
}
#[cfg(not(target_os = "windows"))]
{
cmd = Command::new("npm")
}
cmd
.arg("list")
.arg(name)
.args(&["version", "--depth", "0"])
.current_dir(app_dir)
.output()?
};
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let regex = 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 mut cmd;
#[cfg(target_os = "windows")]
{
cmd = Command::new("cmd");
cmd.arg("/c").arg(command);
}
#[cfg(not(target_os = "windows"))]
{
cmd = Command::new(command)
}
let output = cmd.args(args).arg("--version").output()?;
let version = if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).replace("\n", ""))
} else {
None
};
Ok(version)
}
#[cfg(windows)]
fn webview2_version() -> crate::Result<Option<String>> {
let output = Command::new("powershell")
.args(&["-NoProfile", "-Command"])
.arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
.output()?;
let version = if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).replace("\n", ""))
} else {
// check 32bit installation
let output = Command::new("powershell")
.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() {
Some(String::from_utf8_lossy(&output.stdout).replace("\n", ""))
} else {
None
}
};
Ok(version)
}
struct InfoBlock {
section: bool,
key: &'static str,
value: Option<String>,
suffix: Option<String>,
}
impl InfoBlock {
fn new(key: &'static str) -> Self {
Self {
section: false,
key,
value: None,
suffix: None,
}
}
fn section(mut self) -> Self {
self.section = true;
self
}
fn value<V: Into<Option<String>>>(mut self, value: V) -> Self {
self.value = value.into();
self
}
fn suffix<S: Into<Option<String>>>(mut self, suffix: S) -> Self {
self.suffix = suffix.into();
self
}
fn display(&self) {
if self.section {
println!();
}
print!("{}", self.key);
if let Some(value) = &self.value {
print!(" - {}", value);
}
if let Some(suffix) = &self.suffix {
print!("{}", suffix);
}
println!();
}
}
struct VersionBlock {
section: bool,
key: &'static str,
version: Option<String>,
target_version: Option<String>,
}
impl VersionBlock {
fn new<V: Into<Option<String>>>(key: &'static str, version: V) -> Self {
Self {
section: false,
key,
version: version.into(),
target_version: None,
}
}
fn target_version<V: Into<Option<String>>>(mut self, version: V) -> Self {
self.target_version = version.into();
self
}
fn display(&self) {
if self.section {
println!();
}
print!("{}", self.key);
if let Some(version) = &self.version {
print!(" - {}", version);
} else {
print!(" - Not installed");
}
if let (Some(version), Some(target_version)) = (&self.version, &self.target_version) {
let version = semver::Version::parse(version).unwrap();
let target_version = semver::Version::parse(target_version).unwrap();
if version < target_version {
print!(" (outdated, latest: {})", target_version);
}
}
println!();
}
}
impl Info {
pub fn new() -> Self {
Default::default()
}
pub fn run(self) -> crate::Result<()> {
let os_info = os_info::get();
InfoBlock {
section: true,
key: "Operating System",
value: Some(format!(
"{}, version {} {:?}",
os_info.os_type(),
os_info.version(),
os_info.bitness()
)),
suffix: None,
}
.display();
#[cfg(windows)]
VersionBlock::new("Webview2", webview2_version().unwrap_or_default()).display();
let hook = panic::take_hook();
panic::set_hook(Box::new(|_info| {
// do nothing
}));
let app_dir = panic::catch_unwind(app_dir).map(Some).unwrap_or_default();
panic::set_hook(hook);
let use_yarn = app_dir
.map(|dir| dir.join("yarn.lock").exists())
.unwrap_or_default();
if let Some(node_version) = get_version("node", &[]).unwrap_or_default() {
InfoBlock::new("Node.js environment").section().display();
let metadata = serde_json::from_str::<VersionMetadata>(include_str!("../metadata.json"))?;
VersionBlock::new(
" Node.js",
node_version.chars().skip(1).collect::<String>(),
)
.target_version(metadata.js_cli.node.replace(">= ", ""))
.display();
VersionBlock::new(" @tauri-apps/cli", metadata.js_cli.version)
.target_version(npm_latest_version(use_yarn, "@tauri-apps/cli").unwrap_or_default())
.display();
if let Some(app_dir) = &app_dir {
VersionBlock::new(
" @tauri-apps/api",
npm_package_version(use_yarn, "@tauri-apps/api", app_dir).unwrap_or_default(),
)
.target_version(npm_latest_version(use_yarn, "@tauri-apps/api").unwrap_or_default())
.display();
}
InfoBlock::new("Global packages").section().display();
VersionBlock::new(" npm", get_version("npm", &[]).unwrap_or_default()).display();
VersionBlock::new(" yarn", get_version("yarn", &[]).unwrap_or_default()).display();
}
InfoBlock::new("Rust environment").section().display();
VersionBlock::new(
" rustc",
get_version("rustc", &[]).unwrap_or_default().map(|v| {
let mut s = v.split(' ');
s.next();
s.next().unwrap().to_string()
}),
)
.display();
VersionBlock::new(
" cargo",
get_version("cargo", &[]).unwrap_or_default().map(|v| {
let mut s = v.split(' ');
s.next();
s.next().unwrap().to_string()
}),
)
.display();
if let Some(app_dir) = app_dir {
InfoBlock::new("App directory structure")
.section()
.display();
for entry in read_dir(app_dir)? {
let entry = entry?;
if entry.path().is_dir() {
println!("/{}", entry.path().file_name().unwrap().to_string_lossy());
}
}
InfoBlock::new("App").section().display();
let tauri_dir = tauri_dir();
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> =
if let Ok(lock_contents) = read_to_string(tauri_dir.join("Cargo.lock")) {
toml::from_str(&lock_contents).ok()
} else {
None
};
let tauri_lock_packages: Vec<CargoLockPackage> = lock
.as_ref()
.map(|lock| {
lock
.package
.iter()
.filter(|p| p.name == "tauri")
.cloned()
.collect()
})
.unwrap_or_default();
let (tauri_version_string, found_tauri_versions) =
match (&manifest, &lock, tauri_lock_packages.len()) {
(Some(_manifest), Some(_lock), 1) => {
let tauri_lock_package = tauri_lock_packages.first().unwrap();
(
tauri_lock_package.version.clone(),
vec![tauri_lock_package.version.clone()],
)
}
(None, Some(_lock), 1) => {
let tauri_lock_package = tauri_lock_packages.first().unwrap();
(
format!("{} (no manifest)", tauri_lock_package.version),
vec![tauri_lock_package.version.clone()],
)
}
_ => {
let mut found_tauri_versions = Vec::new();
let manifest_version = match manifest.and_then(|m| m.dependencies.get("tauri").cloned())
{
Some(tauri) => match tauri {
CargoManifestDependency::Version(v) => {
found_tauri_versions.push(v.clone());
v
}
CargoManifestDependency::Package(p) => {
if let Some(v) = p.version {
found_tauri_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 {
"unknown manifest".to_string()
}
}
},
None => "no manifest".to_string(),
};
let lock_version = match (lock, tauri_lock_packages.is_empty()) {
(Some(_lock), true) => tauri_lock_packages
.iter()
.map(|p| p.version.clone())
.collect::<Vec<String>>()
.join(", "),
(Some(_lock), false) => "unknown lockfile".to_string(),
_ => "no lockfile".to_string(),
};
(
format!("{} ({})", manifest_version, lock_version),
found_tauri_versions,
)
}
};
let tauri_version = found_tauri_versions
.into_iter()
.map(|v| semver::Version::parse(&v).unwrap())
.max();
let suffix = match (tauri_version, crate_latest_version("tauri")) {
(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,
};
InfoBlock::new(" tauri.rs")
.value(tauri_version_string)
.suffix(suffix)
.display();
if let Ok(config) = get_config(None) {
let config_guard = config.lock().unwrap();
let config = config_guard.as_ref().unwrap();
InfoBlock::new(" build-type")
.value(if config.tauri.bundle.active {
"bundle".to_string()
} else {
"build".to_string()
})
.display();
InfoBlock::new(" CSP")
.value(if let Some(security) = &config.tauri.security {
security.csp.clone().unwrap_or_else(|| "unset".to_string())
} else {
"unset".to_string()
})
.display();
InfoBlock::new(" distDir")
.value(config.build.dist_dir.to_string())
.display();
InfoBlock::new(" devPath")
.value(config.build.dev_path.to_string())
.display();
}
if let Ok(package_json) = read_to_string(app_dir.join("package.json")) {
let framework_map = [
("svelte", "Svelte", None),
("@angular", "Angular", Some("Webpack")),
(r#""next""#, "React (Next.js)", Some("Webpack")),
("gatsby", "React (Gatsby)", Some("Webpack")),
("react", "React", None),
("nuxt", "Vue.js (Nuxt)", Some("Webpack")),
("quasar", "Vue.js (Quasar)", Some("Webpack")),
("@vue/cli", "Vue.js (Vue CLI)", Some("Webpack")),
("vue", "Vue.js", None),
];
let bundler_map = [("webpack", "Webpack"), ("rollup", "Rollup")];
let (framework, framework_bundler) = framework_map
.iter()
.find(|(keyword, _, _)| package_json.contains(keyword))
.map(|(_, framework, bundler)| {
(Some(framework.to_string()), bundler.map(|b| b.to_string()))
})
.unwrap_or((None, None));
let bundler = bundler_map
.iter()
.find(|(keyword, _)| package_json.contains(keyword))
.map(|(_, bundler)| bundler.to_string())
.or(framework_bundler);
if let Some(framework) = framework {
InfoBlock::new(" framework").value(framework).display();
}
if let Some(bundler) = bundler {
InfoBlock::new(" bundler").value(bundler).display();
}
} else {
println!("package.json not found");
}
}
Ok(())
}
}