mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-01 03:02:28 +03:00
Merge branch 'dev' into next
This commit is contained in:
commit
55900a2968
@ -1,6 +0,0 @@
|
||||
---
|
||||
"api": minor
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Added `raw` encoding option to read stdout and stderr raw bytes.
|
@ -1449,7 +1449,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Download the bootstrapper and run it. Requires internet connection. Results in a smaller installer size, but is not recommended on Windows 7.",
|
||||
"description": "Download the bootstrapper and run it. Requires an internet connection. Results in a smaller installer size, but is not recommended on Windows 7.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
@ -1470,7 +1470,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Embed the bootstrapper and run it. Requires internet connection. Increases the installer size by around 1.8MB, but offers better support on Windows 7.",
|
||||
"description": "Embed the bootstrapper and run it. Requires an internet connection. Increases the installer size by around 1.8MB, but offers better support on Windows 7.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
@ -1491,7 +1491,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Embed the offline installer and run it. Does not require internet connection. Increases the installer size by around 127MB.",
|
||||
"description": "Embed the offline installer and run it. Does not require an internet connection. Increases the installer size by around 127MB.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
|
@ -2602,21 +2602,7 @@ fn handle_user_message<T: UserEvent>(
|
||||
let _ = webview.print();
|
||||
}
|
||||
}
|
||||
WebviewMessage::WebviewEvent(event) => {
|
||||
let window_event_listeners = windows
|
||||
.borrow()
|
||||
.get(&id)
|
||||
.map(|w| w.window_event_listeners.clone());
|
||||
if let Some(window_event_listeners) = window_event_listeners {
|
||||
if let Some(event) = WindowEventWrapper::from(&event).0 {
|
||||
let listeners = window_event_listeners.lock().unwrap();
|
||||
let handlers = listeners.values();
|
||||
for handler in handlers {
|
||||
handler(&event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WebviewMessage::WebviewEvent(_event) => { /* already handled */ }
|
||||
},
|
||||
Message::CreateWebview(window_id, handler) => match handler(event_loop, web_context) {
|
||||
Ok(webview) => {
|
||||
@ -2910,6 +2896,24 @@ fn handle_event_loop<T: UserEvent>(
|
||||
global_listener(id.0, &event);
|
||||
}
|
||||
}
|
||||
Event::UserEvent(Message::Webview(id, WebviewMessage::WebviewEvent(event))) => {
|
||||
if let Some(event) = WindowEventWrapper::from(&event).0 {
|
||||
let windows = windows.borrow();
|
||||
let window = windows.get(&id);
|
||||
if let Some(window) = window {
|
||||
callback(RunEvent::WindowEvent {
|
||||
label: window.label.clone(),
|
||||
event: event.clone(),
|
||||
});
|
||||
|
||||
let listeners = window.window_event_listeners.lock().unwrap();
|
||||
let handlers = listeners.values();
|
||||
for handler in handlers {
|
||||
handler(&event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => {
|
||||
|
@ -506,7 +506,7 @@ pub enum WebviewInstallMode {
|
||||
/// Do not install the Webview2 as part of the Windows Installer.
|
||||
Skip,
|
||||
/// Download the bootstrapper and run it.
|
||||
/// Requires internet connection.
|
||||
/// Requires an internet connection.
|
||||
/// Results in a smaller installer size, but is not recommended on Windows 7.
|
||||
DownloadBootstrapper {
|
||||
/// Instructs the installer to run the bootstrapper in silent mode. Defaults to `true`.
|
||||
@ -514,7 +514,7 @@ pub enum WebviewInstallMode {
|
||||
silent: bool,
|
||||
},
|
||||
/// Embed the bootstrapper and run it.
|
||||
/// Requires internet connection.
|
||||
/// Requires an internet connection.
|
||||
/// Increases the installer size by around 1.8MB, but offers better support on Windows 7.
|
||||
EmbedBootstrapper {
|
||||
/// Instructs the installer to run the bootstrapper in silent mode. Defaults to `true`.
|
||||
@ -522,7 +522,7 @@ pub enum WebviewInstallMode {
|
||||
silent: bool,
|
||||
},
|
||||
/// Embed the offline installer and run it.
|
||||
/// Does not require internet connection.
|
||||
/// Does not require an internet connection.
|
||||
/// Increases the installer size by around 127MB.
|
||||
OfflineInstaller {
|
||||
/// Instructs the installer to run the installer in silent mode. Defaults to `true`.
|
||||
|
@ -73,7 +73,7 @@ os_pipe = { version = "1.0", optional = true }
|
||||
raw-window-handle = "0.5"
|
||||
minisign-verify = { version = "0.2", optional = true }
|
||||
time = { version = "0.3", features = [ "parsing", "formatting" ], optional = true }
|
||||
os_info = { version = "3.5", optional = true }
|
||||
os_info = { version = "3", optional = true }
|
||||
regex = { version = "1.6.0", optional = true }
|
||||
glob = "0.3"
|
||||
data-url = { version = "0.2", optional = true }
|
||||
@ -85,7 +85,7 @@ encoding_rs = "0.8.31"
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
rfd = { version = "0.11", optional = true, features = [ "gtk3", "common-controls-v6" ] }
|
||||
notify-rust = { version = "4.5", default-features = false, features = [ "d" ], optional = true }
|
||||
notify-rust = { version = "4.5", optional = true }
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
gtk = { version = "0.16", features = [ "v3_24" ] }
|
||||
|
@ -216,8 +216,9 @@ impl Cmd {
|
||||
#[module_command_handler(window_create)]
|
||||
async fn create_webview<R: Runtime>(
|
||||
context: InvokeContext<R>,
|
||||
options: Box<WindowConfig>,
|
||||
mut options: Box<WindowConfig>,
|
||||
) -> super::Result<()> {
|
||||
options.additional_browser_args = None;
|
||||
crate::window::WindowBuilder::from_config(&context.window, *options)
|
||||
.build()
|
||||
.map_err(crate::error::into_anyhow)?;
|
||||
|
@ -384,18 +384,16 @@ impl<R: Runtime> UpdateBuilder<R> {
|
||||
// If we got a success, we stop the loop
|
||||
// and we set our remote_release variable
|
||||
if let Ok(res) = resp {
|
||||
let res = res.read().await?;
|
||||
let status = res.status();
|
||||
// got status code 2XX
|
||||
if StatusCode::from_u16(res.status)
|
||||
.map_err(|e| Error::Builder(e.to_string()))?
|
||||
.is_success()
|
||||
{
|
||||
if status.is_success() {
|
||||
// if we got 204
|
||||
if StatusCode::NO_CONTENT.as_u16() == res.status {
|
||||
if status == StatusCode::NO_CONTENT {
|
||||
// return with `UpToDate` error
|
||||
// we should catch on the client
|
||||
return Err(Error::UpToDate);
|
||||
};
|
||||
let res = res.read().await?;
|
||||
// Convert the remote result to our local struct
|
||||
let built_release = serde_json::from_value(res.data).map_err(Into::into);
|
||||
// make sure all went well and the remote data is compatible
|
||||
|
@ -7,9 +7,18 @@
|
||||
fn main() {
|
||||
let mut context = tauri::generate_context!();
|
||||
if std::env::var("TARGET").unwrap_or_default() == "nsis" {
|
||||
// /D sets the default installation directory ($INSTDIR),
|
||||
// overriding InstallDir and InstallDirRegKey.
|
||||
// It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces.
|
||||
// Only absolute paths are supported.
|
||||
// NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder
|
||||
context.config_mut().tauri.updater.windows.installer_args = vec![format!(
|
||||
"/D={}",
|
||||
std::env::current_exe().unwrap().parent().unwrap().display()
|
||||
tauri::utils::platform::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.display()
|
||||
)];
|
||||
}
|
||||
tauri::Builder::default()
|
||||
|
@ -7,7 +7,7 @@ license = ""
|
||||
repository = ""
|
||||
default-run = "app"
|
||||
edition = "2021"
|
||||
rust-version = "1.59"
|
||||
rust-version = "1.60"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { workspace = true }
|
||||
|
@ -57,11 +57,11 @@ interface Duration {
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface ClientOptions {
|
||||
maxRedirections?: number
|
||||
/**
|
||||
* Defines the maximum number of redirects the client should follow.
|
||||
* If set to 0, no redirects will be followed.
|
||||
*/
|
||||
maxRedirections?: number
|
||||
connectTimeout?: number | Duration
|
||||
}
|
||||
|
||||
|
@ -772,7 +772,7 @@ async function extname(path: string): Promise<string> {
|
||||
* import { basename, resolveResource } from '@tauri-apps/api/path';
|
||||
* const resourcePath = await resolveResource('app.conf');
|
||||
* const base = await basename(resourcePath);
|
||||
* assert(base === 'app');
|
||||
* assert(base === 'app.conf');
|
||||
* ```
|
||||
*
|
||||
* @param ext An optional file extension to be removed from the returned path.
|
||||
|
@ -2196,10 +2196,6 @@ interface WindowOptions {
|
||||
* The user agent for the webview.
|
||||
*/
|
||||
userAgent?: string
|
||||
/**
|
||||
* Additional arguments for the webview. **Windows Only**
|
||||
*/
|
||||
additionalBrowserArguments?: string
|
||||
}
|
||||
|
||||
function mapMonitor(m: Monitor | null): Monitor | null {
|
||||
|
@ -96,7 +96,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
"--volname",
|
||||
product_name,
|
||||
"--icon",
|
||||
product_name,
|
||||
&bundle_file_name,
|
||||
"180",
|
||||
"170",
|
||||
"--app-drop-link",
|
||||
|
@ -113,23 +113,25 @@ pub fn extract_zip(data: &[u8], path: &Path) -> crate::Result<()> {
|
||||
for i in 0..zipa.len() {
|
||||
let mut file = zipa.by_index(i)?;
|
||||
|
||||
let dest_path = path.join(file.name());
|
||||
if file.is_dir() {
|
||||
create_dir_all(&dest_path)?;
|
||||
continue;
|
||||
if let Some(name) = file.enclosed_name() {
|
||||
let dest_path = path.join(name);
|
||||
if file.is_dir() {
|
||||
create_dir_all(&dest_path)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let parent = dest_path.parent().expect("Failed to get parent");
|
||||
|
||||
if !parent.exists() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut buff: Vec<u8> = Vec::new();
|
||||
file.read_to_end(&mut buff)?;
|
||||
let mut fileout = File::create(dest_path).expect("Failed to open file");
|
||||
|
||||
fileout.write_all(&buff)?;
|
||||
}
|
||||
|
||||
let parent = dest_path.parent().expect("Failed to get parent");
|
||||
|
||||
if !parent.exists() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut buff: Vec<u8> = Vec::new();
|
||||
file.read_to_end(&mut buff)?;
|
||||
let mut fileout = File::create(dest_path).expect("Failed to open file");
|
||||
|
||||
fileout.write_all(&buff)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -70,7 +70,7 @@ include_dir = "0.7"
|
||||
minisign = "0.7"
|
||||
base64 = "0.21.0"
|
||||
ureq = "2.5"
|
||||
os_info = "3.5"
|
||||
os_info = "3"
|
||||
semver = "1.0"
|
||||
regex = "1.6.0"
|
||||
unicode-width = "0.1"
|
||||
|
@ -1449,7 +1449,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Download the bootstrapper and run it. Requires internet connection. Results in a smaller installer size, but is not recommended on Windows 7.",
|
||||
"description": "Download the bootstrapper and run it. Requires an internet connection. Results in a smaller installer size, but is not recommended on Windows 7.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
@ -1470,7 +1470,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Embed the bootstrapper and run it. Requires internet connection. Increases the installer size by around 1.8MB, but offers better support on Windows 7.",
|
||||
"description": "Embed the bootstrapper and run it. Requires an internet connection. Increases the installer size by around 1.8MB, but offers better support on Windows 7.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
@ -1491,7 +1491,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Embed the offline installer and run it. Does not require internet connection. Increases the installer size by around 127MB.",
|
||||
"description": "Embed the offline installer and run it. Does not require an internet connection. Increases the installer size by around 127MB.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
|
Binary file not shown.
@ -313,7 +313,7 @@ pub fn setup(options: &mut Options, mobile: bool) -> Result<AppInterface> {
|
||||
use crate::helpers::web_dev_server::start_dev_server;
|
||||
if path.exists() {
|
||||
let path = path.canonicalize()?;
|
||||
let server_url = start_dev_server(path, options.port);
|
||||
let server_url = start_dev_server(path, options.port)?;
|
||||
let server_url = format!("http://{server_url}");
|
||||
dev_path = AppUrl::Url(WindowUrl::External(server_url.parse().unwrap()));
|
||||
|
||||
|
@ -31,17 +31,10 @@ struct State {
|
||||
tx: Sender<()>,
|
||||
}
|
||||
|
||||
pub fn start_dev_server<P: AsRef<Path>>(path: P, port: Option<u16>) -> SocketAddr {
|
||||
pub fn start_dev_server<P: AsRef<Path>>(path: P, port: Option<u16>) -> crate::Result<SocketAddr> {
|
||||
let serve_dir = path.as_ref().to_path_buf();
|
||||
let server_url = SocketAddr::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1).into(),
|
||||
port.unwrap_or_else(|| {
|
||||
std::env::var("TAURI_DEV_SERVER_PORT")
|
||||
.unwrap_or_else(|_| "1430".to_string())
|
||||
.parse()
|
||||
.unwrap()
|
||||
}),
|
||||
);
|
||||
|
||||
let (server_url_tx, server_url_rx) = std::sync::mpsc::channel();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
@ -74,6 +67,32 @@ pub fn start_dev_server<P: AsRef<Path>>(path: P, port: Option<u16>) -> SocketAdd
|
||||
}
|
||||
});
|
||||
|
||||
let mut auto_port = false;
|
||||
let mut port = port.unwrap_or_else(|| {
|
||||
std::env::var("TAURI_DEV_SERVER_PORT")
|
||||
.unwrap_or_else(|_| {
|
||||
auto_port = true;
|
||||
"1430".to_string()
|
||||
})
|
||||
.parse()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let (server, server_url) = loop {
|
||||
let server_url = SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), port);
|
||||
let server = Server::try_bind(&server_url);
|
||||
|
||||
if !auto_port {
|
||||
break (server, server_url);
|
||||
}
|
||||
|
||||
if server.is_ok() {
|
||||
break (server, server_url);
|
||||
}
|
||||
|
||||
port += 1;
|
||||
};
|
||||
|
||||
let state = Arc::new(State {
|
||||
serve_dir,
|
||||
tx,
|
||||
@ -96,14 +115,24 @@ pub fn start_dev_server<P: AsRef<Path>>(path: P, port: Option<u16>) -> SocketAdd
|
||||
ws.on_upgrade(|socket| async move { ws_handler(socket, state).await })
|
||||
}),
|
||||
);
|
||||
Server::bind(&server_url)
|
||||
.serve(router.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match server {
|
||||
Ok(server) => {
|
||||
server_url_tx.send(Ok(server_url)).unwrap();
|
||||
server.serve(router.into_make_service()).await.unwrap();
|
||||
}
|
||||
Err(e) => {
|
||||
server_url_tx
|
||||
.send(Err(anyhow::anyhow!(
|
||||
"failed to start development server on {server_url}: {e}"
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
server_url
|
||||
server_url_rx.recv().unwrap()
|
||||
}
|
||||
|
||||
async fn handler<T>(req: Request<T>, state: Arc<State>) -> impl IntoResponse {
|
||||
|
@ -1,959 +0,0 @@
|
||||
// Copyright 2019-2023 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', '\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', '\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!("{version_string} (no manifest)"),
|
||||
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).ok())
|
||||
.max();
|
||||
let suffix = match (crate_version, crate_latest_version(name)) {
|
||||
(Some(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!("{} {}", "-".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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if tauri_dir.is_some() {
|
||||
let p = tauri_dir.as_ref().unwrap();
|
||||
if p.join("gen/apple").exists() {
|
||||
let teams = tauri_mobile::apple::teams::find_development_teams().unwrap_or_default();
|
||||
Section("iOS").display();
|
||||
InfoBlock::new(
|
||||
"Teams",
|
||||
if teams.is_empty() {
|
||||
"None".red().to_string()
|
||||
} else {
|
||||
teams
|
||||
.iter()
|
||||
.map(|t| format!("{} (ID: {})", t.name, t.id))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
},
|
||||
)
|
||||
.display();
|
||||
}
|
||||
}
|
||||
|
||||
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 manager 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)
|
||||
}
|
||||
}
|
80
tooling/cli/src/info/app.rs
Normal file
80
tooling/cli/src/info/app.rs
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{SectionItem, Status};
|
||||
use crate::helpers::framework;
|
||||
use std::{
|
||||
fs::read_to_string,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<SectionItem> {
|
||||
let mut items = Vec::new();
|
||||
if tauri_dir.is_some() {
|
||||
if let Ok(config) = crate::helpers::config::get(None) {
|
||||
let config_guard = config.lock().unwrap();
|
||||
let config = config_guard.as_ref().unwrap();
|
||||
|
||||
let bundle_or_build = if config.tauri.bundle.active {
|
||||
"bundle".to_string()
|
||||
} else {
|
||||
"build".to_string()
|
||||
};
|
||||
items.push(SectionItem::new(
|
||||
move || Some((format!("build-type: {bundle_or_build}"), Status::Neutral)),
|
||||
|| None,
|
||||
false,
|
||||
));
|
||||
|
||||
let csp = config
|
||||
.tauri
|
||||
.security
|
||||
.csp
|
||||
.clone()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "unset".to_string());
|
||||
items.push(SectionItem::new(
|
||||
move || Some((format!("CSP: {csp}"), Status::Neutral)),
|
||||
|| None,
|
||||
false,
|
||||
));
|
||||
|
||||
let dist_dir = config.build.dist_dir.to_string();
|
||||
items.push(SectionItem::new(
|
||||
move || Some((format!("distDir: {dist_dir}"), Status::Neutral)),
|
||||
|| None,
|
||||
false,
|
||||
));
|
||||
|
||||
let dev_path = config.build.dev_path.to_string();
|
||||
items.push(SectionItem::new(
|
||||
move || Some((format!("devPath: {dev_path}"), Status::Neutral)),
|
||||
|| None,
|
||||
false,
|
||||
));
|
||||
|
||||
if let Some(app_dir) = app_dir {
|
||||
if let Ok(package_json) = read_to_string(app_dir.join("package.json")) {
|
||||
let (framework, bundler) = framework::infer_from_package_json(&package_json);
|
||||
if let Some(framework) = framework {
|
||||
items.push(SectionItem::new(
|
||||
move || Some((format!("framework: {framework}"), Status::Neutral)),
|
||||
|| None,
|
||||
false,
|
||||
));
|
||||
}
|
||||
if let Some(bundler) = bundler {
|
||||
items.push(SectionItem::new(
|
||||
move || Some((format!("bundler: {bundler}"), Status::Neutral)),
|
||||
|| None,
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
125
tooling/cli/src/info/env_nodejs.rs
Normal file
125
tooling/cli/src/info/env_nodejs.rs
Normal file
@ -0,0 +1,125 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{cross_command, VersionMetadata};
|
||||
use super::{SectionItem, Status};
|
||||
use colored::Colorize;
|
||||
|
||||
pub fn items(metadata: &VersionMetadata) -> (Vec<SectionItem>, Option<String>) {
|
||||
let yarn_version = cross_command("yarn")
|
||||
.arg("-v")
|
||||
.output()
|
||||
.map(|o| {
|
||||
if o.status.success() {
|
||||
let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
|
||||
Some(v.split('\n').next().unwrap().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let yarn_version_c = yarn_version.clone();
|
||||
let node_target_ver = metadata.js_cli.node.replace(">= ", "");
|
||||
|
||||
(
|
||||
vec![
|
||||
SectionItem::new(
|
||||
move || {
|
||||
cross_command("node")
|
||||
.arg("-v")
|
||||
.output()
|
||||
.map(|o| {
|
||||
if o.status.success() {
|
||||
let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
|
||||
let v = v
|
||||
.split('\n')
|
||||
.next()
|
||||
.unwrap()
|
||||
.strip_prefix('v')
|
||||
.unwrap_or_default()
|
||||
.trim();
|
||||
Some((
|
||||
format!("node: {}{}", v, {
|
||||
let version = semver::Version::parse(v).unwrap();
|
||||
let target_version = semver::Version::parse(node_target_ver.as_str()).unwrap();
|
||||
if version < target_version {
|
||||
format!(
|
||||
" ({}, latest: {})",
|
||||
"outdated".red(),
|
||||
target_version.to_string().green()
|
||||
)
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}),
|
||||
Status::Neutral,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
SectionItem::new(
|
||||
|| {
|
||||
cross_command("pnpm")
|
||||
.arg("-v")
|
||||
.output()
|
||||
.map(|o| {
|
||||
if o.status.success() {
|
||||
let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
|
||||
Some((
|
||||
format!("pnpm: {}", v.split('\n').next().unwrap()),
|
||||
Status::Neutral,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
SectionItem::new(
|
||||
move || {
|
||||
yarn_version_c
|
||||
.as_ref()
|
||||
.map(|v| (format!("yarn: {v}"), Status::Neutral))
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
SectionItem::new(
|
||||
|| {
|
||||
cross_command("npm")
|
||||
.arg("-v")
|
||||
.output()
|
||||
.map(|o| {
|
||||
if o.status.success() {
|
||||
let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
|
||||
Some((
|
||||
format!("npm: {}", v.split('\n').next().unwrap()),
|
||||
Status::Neutral,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
],
|
||||
yarn_version,
|
||||
)
|
||||
}
|
145
tooling/cli/src/info/env_rust.rs
Normal file
145
tooling/cli/src/info/env_rust.rs
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::SectionItem;
|
||||
use super::Status;
|
||||
use colored::Colorize;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn items() -> Vec<SectionItem> {
|
||||
vec![
|
||||
SectionItem::new(
|
||||
|| {
|
||||
Some(
|
||||
Command::new("rustc")
|
||||
.arg("-V")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
|
||||
.map(|v| {
|
||||
format!(
|
||||
"rustc: {}",
|
||||
v.split('\n')
|
||||
.next()
|
||||
.unwrap()
|
||||
.strip_prefix("rustc ")
|
||||
.unwrap_or_default()
|
||||
)
|
||||
})
|
||||
.map(|desc| (desc, Status::Success))
|
||||
.ok()
|
||||
.unwrap_or_else(|| {
|
||||
(
|
||||
format!(
|
||||
"rustc: {}\nmaybe you don't have rust installed! Visist {}",
|
||||
"not installed!".red(),
|
||||
"https://rustup.rs/".cyan()
|
||||
),
|
||||
Status::Error,
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
SectionItem::new(
|
||||
|| {
|
||||
Some(
|
||||
Command::new("cargo")
|
||||
.arg("-V")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
|
||||
.map(|v| {
|
||||
format!(
|
||||
"Cargo: {}",
|
||||
v.split('\n')
|
||||
.next()
|
||||
.unwrap()
|
||||
.strip_prefix("cargo ")
|
||||
.unwrap_or_default()
|
||||
)
|
||||
})
|
||||
.map(|desc| (desc, Status::Success))
|
||||
.ok()
|
||||
.unwrap_or_else(|| {
|
||||
(
|
||||
format!(
|
||||
"Cargo: {}\nmaybe you don't have rust installed! Visit {}",
|
||||
"not installed!".red(),
|
||||
"https://rustup.rs/".cyan()
|
||||
),
|
||||
Status::Error,
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
SectionItem::new(
|
||||
|| {
|
||||
Some(
|
||||
Command::new("rustup")
|
||||
.arg("-V")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
|
||||
.map(|v| {
|
||||
format!(
|
||||
"rustup: {}",
|
||||
v.split('\n')
|
||||
.next()
|
||||
.unwrap()
|
||||
.strip_prefix("rustup ")
|
||||
.unwrap_or_default()
|
||||
)
|
||||
})
|
||||
.map(|desc| (desc, Status::Success))
|
||||
.ok()
|
||||
.unwrap_or_else(|| {
|
||||
(
|
||||
format!(
|
||||
"rustup: {}\nIf you have rust installed some other way, we recommend uninstalling it\nthen use rustup instead. Visit {}",
|
||||
"not installed!".red(),
|
||||
"https://rustup.rs/".cyan()
|
||||
),
|
||||
Status::Warning,
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
SectionItem::new(
|
||||
|| {
|
||||
Some(
|
||||
Command::new("rustup")
|
||||
.args(["show", "active-toolchain"])
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
|
||||
.map(|v| {
|
||||
format!(
|
||||
"Rust toolchain: {}",
|
||||
v.split('\n')
|
||||
.next()
|
||||
.unwrap()
|
||||
)
|
||||
})
|
||||
.map(|desc| (desc, Status::Success))
|
||||
.ok()
|
||||
.unwrap_or_else(|| {
|
||||
(
|
||||
format!(
|
||||
"Rust toolchain: couldn't be deteceted!\nmaybe you don't have rustup installed? if so, Visit {}", "https://rustup.rs/".cyan()
|
||||
),
|
||||
Status::Warning,
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
]
|
||||
}
|
299
tooling/cli/src/info/env_system.rs
Normal file
299
tooling/cli/src/info/env_system.rs
Normal file
@ -0,0 +1,299 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::SectionItem;
|
||||
use super::Status;
|
||||
use colored::Colorize;
|
||||
#[cfg(windows)]
|
||||
use serde::Deserialize;
|
||||
use std::process::Command;
|
||||
|
||||
#[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 = Command::new(vswhere)
|
||||
.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
|
||||
})
|
||||
}
|
||||
|
||||
#[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(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
fn pkg_conf_version(package: &str) -> Option<String> {
|
||||
Command::new("pkg-config")
|
||||
.args([package, "--print-provides"])
|
||||
.output()
|
||||
.map(|o| {
|
||||
String::from_utf8_lossy(&o.stdout)
|
||||
.split('=')
|
||||
.nth(1)
|
||||
.map(|s| s.trim().to_string())
|
||||
})
|
||||
.unwrap_or(None)
|
||||
}
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
fn webkit2gtk_ver() -> Option<String> {
|
||||
pkg_conf_version("webkit2gtk-4.0")
|
||||
}
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
fn rsvg2_ver() -> Option<String> {
|
||||
pkg_conf_version("librsvg-2.0")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn is_xcode_command_line_tools_installed() -> bool {
|
||||
Command::new("xcode-select")
|
||||
.arg("-p")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn items() -> Vec<SectionItem> {
|
||||
vec![
|
||||
SectionItem::new(
|
||||
|| {
|
||||
let os_info = os_info::get();
|
||||
Some((
|
||||
format!(
|
||||
"OS: {} {} {:?}",
|
||||
os_info.os_type(),
|
||||
os_info.version(),
|
||||
os_info.bitness()
|
||||
),
|
||||
Status::Neutral,
|
||||
))
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
#[cfg(windows)]
|
||||
SectionItem::new(
|
||||
|| {
|
||||
let error = || {
|
||||
format!(
|
||||
"Webview2: {}\nVisit {}",
|
||||
"not installed!".red(),
|
||||
"https://developer.microsoft.com/en-us/microsoft-edge/webview2/".cyan()
|
||||
)
|
||||
};
|
||||
Some(
|
||||
webview2_version()
|
||||
.map(|v| {
|
||||
v.map(|v| (format!("WebView2: {}", v), Status::Success))
|
||||
.unwrap_or_else(|| (error(), Status::Error))
|
||||
})
|
||||
.unwrap_or_else(|_| (error(), Status::Error)),
|
||||
)
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
#[cfg(windows)]
|
||||
SectionItem::new(
|
||||
|| {
|
||||
let build_tools = build_tools_version()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
if build_tools.is_empty() {
|
||||
Some((
|
||||
format!(
|
||||
"Couldn't detect Visual Studio or Visual Studio Build Tools. Download from {}",
|
||||
"https://aka.ms/vs/17/release/vs_BuildTools.exe".cyan()
|
||||
),
|
||||
Status::Error,
|
||||
))
|
||||
} else {
|
||||
Some((
|
||||
format!(
|
||||
"MSVC: {}{}",
|
||||
if build_tools.len() > 1 {
|
||||
format!("\n {} ", "-".cyan())
|
||||
} else {
|
||||
"".into()
|
||||
},
|
||||
build_tools.join(format!("\n {} ", "-".cyan()).as_str()),
|
||||
),
|
||||
Status::Success,
|
||||
))
|
||||
}
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
SectionItem::new(
|
||||
|| {
|
||||
Some(
|
||||
webkit2gtk_ver()
|
||||
.map(|v| (format!("webkit2gtk-4.0: {v}"), Status::Success))
|
||||
.unwrap_or_else(|| {
|
||||
(
|
||||
format!(
|
||||
"webkit2gtk-4.0: {}\nVisit {} to learn more about tauri prerequisites",
|
||||
"not installed".red(),
|
||||
"https://tauri.app/v1/guides/getting-started/prerequisites".cyan()
|
||||
),
|
||||
Status::Error,
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
SectionItem::new(
|
||||
|| {
|
||||
Some(
|
||||
rsvg2_ver()
|
||||
.map(|v| (format!("rsvg2: {v}"), Status::Success))
|
||||
.unwrap_or_else(|| {
|
||||
(
|
||||
format!(
|
||||
"rsvg2: {}\nVisit {} to learn more about tauri prerequisites",
|
||||
"not installed".red(),
|
||||
"https://tauri.app/v1/guides/getting-started/prerequisites".cyan()
|
||||
),
|
||||
Status::Error,
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
#[cfg(target_os = "macos")]
|
||||
SectionItem::new(
|
||||
|| {
|
||||
Some(if is_xcode_command_line_tools_installed() {
|
||||
(
|
||||
"Xcode Command Line Tools: installed".into(),
|
||||
Status::Success,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!(
|
||||
"Xcode Command Line Tools: {}\n Run `{}`",
|
||||
"not installed!".red(),
|
||||
"xcode-select --install".cyan()
|
||||
),
|
||||
Status::Error,
|
||||
)
|
||||
})
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
),
|
||||
]
|
||||
}
|
25
tooling/cli/src/info/ios.rs
Normal file
25
tooling/cli/src/info/ios.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use super::{SectionItem, Status};
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
pub fn items() -> Vec<SectionItem> {
|
||||
vec![SectionItem::new(
|
||||
|| {
|
||||
let teams = tauri_mobile::apple::teams::find_development_teams().unwrap_or_default();
|
||||
Some((
|
||||
if teams.is_empty() {
|
||||
"None".red().to_string()
|
||||
} else {
|
||||
teams
|
||||
.iter()
|
||||
.map(|t| format!("{} (ID: {})", t.name, t.id))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
},
|
||||
Status::Neutral,
|
||||
))
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
)]
|
||||
}
|
287
tooling/cli/src/info/mod.rs
Normal file
287
tooling/cli/src/info/mod.rs
Normal file
@ -0,0 +1,287 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::Result;
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
panic,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
mod app;
|
||||
mod env_nodejs;
|
||||
mod env_rust;
|
||||
mod env_system;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod ios;
|
||||
mod packages_nodejs;
|
||||
mod packages_rust;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsCliVersionMetadata {
|
||||
version: String,
|
||||
node: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VersionMetadata {
|
||||
#[serde(rename = "cli.js")]
|
||||
js_cli: JsCliVersionMetadata,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
pub 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
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
pub enum Status {
|
||||
Neutral = 0,
|
||||
#[default]
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Display for Status {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Status::Neutral => "-".cyan(),
|
||||
Status::Success => "✔".green(),
|
||||
Status::Warning => "⚠".yellow(),
|
||||
Status::Error => "✘".red(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SectionItem {
|
||||
/// If description is none, the item is skipped
|
||||
description: Option<String>,
|
||||
status: Status,
|
||||
/// This closure return will be assigned to status and description
|
||||
action: Box<dyn FnMut() -> Option<(String, Status)>>,
|
||||
/// This closure return will be assigned to status and description
|
||||
action_if_err: Box<dyn FnMut() -> Option<(String, Status)>>,
|
||||
has_action_if_err: bool,
|
||||
}
|
||||
|
||||
impl Display for SectionItem {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let desc = self
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|s| s.replace('\n', "\n "))
|
||||
.unwrap_or_default();
|
||||
let (first, second) = desc.split_once(':').unwrap();
|
||||
write!(f, "{} {}:{}", self.status, first.bold(), second)
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionItem {
|
||||
fn new<
|
||||
F1: FnMut() -> Option<(String, Status)> + 'static,
|
||||
F2: FnMut() -> Option<(String, Status)> + 'static,
|
||||
>(
|
||||
action: F1,
|
||||
action_if_err: F2,
|
||||
has_action_if_err: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
action: Box::new(action),
|
||||
action_if_err: Box::new(action_if_err),
|
||||
has_action_if_err,
|
||||
description: None,
|
||||
status: Status::Neutral,
|
||||
}
|
||||
}
|
||||
fn run(&mut self, interactive: bool) -> Status {
|
||||
if let Some(ret) = (self.action)() {
|
||||
self.description = Some(ret.0);
|
||||
self.status = ret.1;
|
||||
}
|
||||
|
||||
if self.status == Status::Error && interactive && self.has_action_if_err {
|
||||
if let Some(description) = &self.description {
|
||||
let confirmed = Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(format!(
|
||||
"{}\n Run the automatic fix?",
|
||||
description.replace('\n', "\n ")
|
||||
))
|
||||
.interact()
|
||||
.unwrap_or(false);
|
||||
if confirmed {
|
||||
if let Some(ret) = (self.action_if_err)() {
|
||||
self.description = Some(ret.0);
|
||||
self.status = ret.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.status
|
||||
}
|
||||
}
|
||||
|
||||
struct Section<'a> {
|
||||
label: &'a str,
|
||||
interactive: bool,
|
||||
items: Vec<SectionItem>,
|
||||
}
|
||||
|
||||
impl Section<'_> {
|
||||
fn display(&mut self) {
|
||||
let mut status = Status::Neutral;
|
||||
|
||||
for item in &mut self.items {
|
||||
let s = item.run(self.interactive);
|
||||
if s > status {
|
||||
status = s;
|
||||
}
|
||||
}
|
||||
|
||||
let status_str = format!("[{status}]");
|
||||
let status = match status {
|
||||
Status::Neutral => status_str.normal(),
|
||||
Status::Success => status_str.green(),
|
||||
Status::Warning => status_str.yellow(),
|
||||
Status::Error => status_str.red(),
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("{} {}", status, self.label.bold().yellow());
|
||||
for item in &self.items {
|
||||
if item.description.is_some() {
|
||||
println!(" {item}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Shows information about Tauri dependencies and project configuration")]
|
||||
pub struct Options {
|
||||
/// Interactive mode to apply automatic fixes.
|
||||
#[clap(long)]
|
||||
pub interactive: bool,
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let Options { interactive } = options;
|
||||
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();
|
||||
let tauri_dir = panic::catch_unwind(crate::helpers::app_paths::tauri_dir)
|
||||
.map(Some)
|
||||
.unwrap_or_default();
|
||||
panic::set_hook(hook);
|
||||
let metadata = version_metadata()?;
|
||||
|
||||
let mut environment = Section {
|
||||
label: "Environment",
|
||||
interactive,
|
||||
items: Vec::new(),
|
||||
};
|
||||
environment.items.extend(env_system::items());
|
||||
environment.items.extend(env_rust::items());
|
||||
let (items, yarn_version) = env_nodejs::items(&metadata);
|
||||
environment.items.extend(items);
|
||||
|
||||
let mut packages = Section {
|
||||
label: "Packages",
|
||||
interactive,
|
||||
items: Vec::new(),
|
||||
};
|
||||
packages
|
||||
.items
|
||||
.extend(packages_rust::items(app_dir, tauri_dir.as_deref()));
|
||||
packages
|
||||
.items
|
||||
.extend(packages_nodejs::items(app_dir, &metadata, yarn_version));
|
||||
|
||||
let mut app = Section {
|
||||
label: "App",
|
||||
interactive,
|
||||
items: Vec::new(),
|
||||
};
|
||||
app.items.extend(app::items(app_dir, tauri_dir.as_deref()));
|
||||
|
||||
environment.display();
|
||||
packages.display();
|
||||
app.display();
|
||||
|
||||
// iOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(p) = &tauri_dir {
|
||||
if p.join("gen/apple").exists() {
|
||||
let mut ios = Section {
|
||||
label: "iOS",
|
||||
interactive,
|
||||
items: Vec::new(),
|
||||
};
|
||||
ios.items.extend(ios::items());
|
||||
ios.display();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
287
tooling/cli/src/info/packages_nodejs.rs
Normal file
287
tooling/cli/src/info/packages_nodejs.rs
Normal file
@ -0,0 +1,287 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{cross_command, VersionMetadata};
|
||||
use super::{SectionItem, Status};
|
||||
use colored::Colorize;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Display;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnVersionInfo {
|
||||
data: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum PackageManager {
|
||||
Npm,
|
||||
Pnpm,
|
||||
Yarn,
|
||||
YarnBerry,
|
||||
}
|
||||
|
||||
impl Display for PackageManager {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
PackageManager::Npm => "npm",
|
||||
PackageManager::Pnpm => "pnpm",
|
||||
PackageManager::Yarn => "yarn",
|
||||
PackageManager::YarnBerry => "yarn berry",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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::YarnBerry => {
|
||||
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::YarnBerry => (
|
||||
cross_command("yarn")
|
||||
.arg("info")
|
||||
.arg(name)
|
||||
.arg("--json")
|
||||
.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("@(\\d[\\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_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> 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!(
|
||||
"{}: no lock files found, defaulting to npm",
|
||||
"WARNING".yellow()
|
||||
);
|
||||
return PackageManager::Npm;
|
||||
}
|
||||
|
||||
let mut found = Vec::new();
|
||||
|
||||
if use_npm {
|
||||
found.push(PackageManager::Npm);
|
||||
}
|
||||
if use_pnpm {
|
||||
found.push(PackageManager::Pnpm);
|
||||
}
|
||||
if use_yarn {
|
||||
found.push(PackageManager::Yarn);
|
||||
}
|
||||
|
||||
if found.len() > 1 {
|
||||
let pkg_manger = found[0];
|
||||
println!(
|
||||
"{}: Only one package manager should be used, but found {}.\n Please remove unused package manager lock files, will use {} for now!",
|
||||
"WARNING".yellow(),
|
||||
found.iter().map(ToString::to_string).collect::<Vec<_>>().join(" and "),
|
||||
pkg_manger
|
||||
);
|
||||
return pkg_manger;
|
||||
}
|
||||
|
||||
if use_npm {
|
||||
PackageManager::Npm
|
||||
} else if use_pnpm {
|
||||
PackageManager::Pnpm
|
||||
} else {
|
||||
PackageManager::Yarn
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items(
|
||||
app_dir: Option<&PathBuf>,
|
||||
metadata: &VersionMetadata,
|
||||
yarn_version: Option<String>,
|
||||
) -> Vec<SectionItem> {
|
||||
let mut package_manager = PackageManager::Npm;
|
||||
if let Some(app_dir) = &app_dir {
|
||||
let app_dir_entries = std::fs::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
|
||||
.map(|v| v.chars().next().map(|c| c > '1').unwrap_or_default())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
package_manager = PackageManager::YarnBerry;
|
||||
}
|
||||
|
||||
let mut items = Vec::new();
|
||||
if let Some(app_dir) = app_dir {
|
||||
for (package, version) in [
|
||||
("@tauri-apps/api", None),
|
||||
("@tauri-apps/cli", Some(metadata.js_cli.version.clone())),
|
||||
] {
|
||||
let app_dir = app_dir.clone();
|
||||
let item = SectionItem::new(
|
||||
move || {
|
||||
let version = version.clone().unwrap_or_else(|| {
|
||||
npm_package_version(&package_manager, package, &app_dir)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let latest_ver = npm_latest_version(&package_manager, package)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
Some((
|
||||
if version.is_empty() {
|
||||
format!("{} {}: not installed!", package, "[NPM]".dimmed())
|
||||
} else {
|
||||
format!(
|
||||
"{} {}: {}{}",
|
||||
package,
|
||||
"[NPM]".dimmed(),
|
||||
version,
|
||||
if !(version.is_empty() || latest_ver.is_empty()) {
|
||||
let version = semver::Version::parse(version.as_str()).unwrap();
|
||||
let target_version = semver::Version::parse(latest_ver.as_str()).unwrap();
|
||||
|
||||
if version < target_version {
|
||||
format!(" ({}, latest: {})", "outdated".yellow(), latest_ver.green())
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
)
|
||||
},
|
||||
Status::Neutral,
|
||||
))
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
);
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
240
tooling/cli/src/info/packages_rust.rs
Normal file
240
tooling/cli/src/info/packages_rust.rs
Normal file
@ -0,0 +1,240 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{SectionItem, Status};
|
||||
use crate::interface::rust::get_workspace_dir;
|
||||
use colored::Colorize;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
struct CargoLockPackage {
|
||||
name: String,
|
||||
version: String,
|
||||
source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CargoLock {
|
||||
package: Vec<CargoLockPackage>,
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
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 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!("{version_string} (no manifest)"),
|
||||
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).ok())
|
||||
.max();
|
||||
let suffix = match (crate_version, crate_latest_version(name)) {
|
||||
(Some(Some(version)), Some(target_version)) => {
|
||||
let target_version = semver::Version::parse(&target_version).unwrap();
|
||||
if version < target_version {
|
||||
Some(format!(
|
||||
" ({}, latest: {})",
|
||||
"outdated".yellow(),
|
||||
target_version.to_string().green()
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
(crate_version_string, suffix)
|
||||
}
|
||||
|
||||
pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<SectionItem> {
|
||||
let mut items = Vec::new();
|
||||
if tauri_dir.is_some() || app_dir.is_some() {
|
||||
if let Some(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> = 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 in ["tauri", "tauri-build", "wry", "tao"] {
|
||||
let (version_string, version_suffix) =
|
||||
crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
|
||||
let dep = dep.to_string();
|
||||
let item = SectionItem::new(
|
||||
move || {
|
||||
Some((
|
||||
format!(
|
||||
"{} {}: {}{}",
|
||||
dep,
|
||||
"[RUST]".dimmed(),
|
||||
version_string,
|
||||
version_suffix
|
||||
.clone()
|
||||
.map(|s| format!(",{s}"))
|
||||
.unwrap_or_else(|| "".into())
|
||||
),
|
||||
Status::Neutral,
|
||||
))
|
||||
},
|
||||
|| None,
|
||||
false,
|
||||
);
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
@ -623,6 +623,7 @@ struct BinarySettings {
|
||||
|
||||
/// The package settings.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct CargoPackageSettings {
|
||||
/// the package's name.
|
||||
pub name: Option<String>,
|
||||
|
@ -31,7 +31,7 @@ pub fn command(mut options: Options) -> Result<()> {
|
||||
options.ci = options.ci || std::env::var("CI").is_ok();
|
||||
|
||||
if options.ci && options.password.is_none() {
|
||||
println!("Generating new private key without password.");
|
||||
log::warn!("Generating new private key without password. For security reasons, we recommend setting a password instead.");
|
||||
options.password.replace("".into());
|
||||
}
|
||||
let keypair = generate_key(options.password).expect("Failed to generate key");
|
||||
|
@ -21,8 +21,11 @@ jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
permissions:
|
||||
issues: write
|
||||
checks: write
|
||||
{{{{/raw}}}}
|
||||
|
@ -17,7 +17,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: install webkit2gtk
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v2
|
||||
|
@ -17,7 +17,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt with stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
|
@ -1,46 +0,0 @@
|
||||
{{{{raw}}}}
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths-ignore:
|
||||
- 'webview-src/**'
|
||||
- 'webview-dist/**'
|
||||
- 'examples/**'
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y webkit2gtk-4.1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path=Cargo.toml --release
|
||||
{{{{/raw}}}}
|
@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "tauri-plugin-{{ plugin_name }}"
|
||||
version = "0.0.0"
|
||||
authors = [ "{{ author }}" ]
|
||||
description = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.64"
|
||||
exclude = ["/examples", "/webview-dist", "/webview-src", "node_modules"]
|
||||
|
||||
[dependencies]
|
||||
tauri = {{{ tauri_dep }}}
|
||||
serde = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = {{{ tauri_build_dep }}}
|
@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "tauri-app"
|
||||
version = "0.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.64"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = {{{ tauri_build_dep }}}
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = {{{ tauri_example_dep }}}
|
||||
tauri-plugin-{{ plugin_name }} = { path = "../../../" }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = [ "{{ author }}" ]
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.64"
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
tauri = {{{ tauri_example_dep }}}
|
||||
tauri-plugin-{{ plugin_name }} = { path = "../../../" }
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = {{{ tauri_build_dep }}}
|
||||
|
||||
[features]
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
Loading…
Reference in New Issue
Block a user