mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-02-06 16:16:56 +00:00
feat(cli): check plugin versions for incompatibilities (#13993)
* feat(cli): check plugin versions for incompatibilities check core plugin versions for incompatibilities between Cargo and NPM releases a plugin NPM/cargo version is considered "incompatible" if their major or minor versions are not equal on dev we show an warning on build we error out (with a `--ignore-incompatible-plugins` flag to prevent that) this is an idea from @oscartbeaumont we've seen several plugin changes that require updates for both the cargo and the NPM releases of a plugin, and if they are not in sync, the functionality does not work e.g. https://github.com/tauri-apps/plugins-workspace/pull/2573 where the change actually breaks the app updater if you miss the NPM update * Use list to get multiple package versions at once * Fix for older rust versions * Clippy * Support yarn classic * Support yarn berry * Use `.cmd` only for `npm`, `yarn`, `pnpm` * Use yarn list without --pattern * rename * Extract function `check_incompatible_packages` * Check `tauri` <-> `@tauri-apps/api` * incompatible -> mismatched * run build check in parallel * rename struct * Switch back to use sync check and add todo * Extract to function `cargo_manifest_and_lock` --------- Co-authored-by: Tony <legendmastertony@gmail.com>
This commit is contained in:
parent
7c2eb31c83
commit
bc4afe7dd4
6
.changes/check-plugin-versions.md
Normal file
6
.changes/check-plugin-versions.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-cli": minor:feat
|
||||
"@tauri-apps/cli": minor:feat
|
||||
---
|
||||
|
||||
Check installed plugin NPM/crate versions for incompatible releases.
|
||||
@ -6,9 +6,10 @@ use crate::{
|
||||
bundle::BundleFormat,
|
||||
helpers::{
|
||||
self,
|
||||
app_paths::tauri_dir,
|
||||
app_paths::{frontend_dir, tauri_dir},
|
||||
config::{get as get_config, ConfigHandle, FrontendDist},
|
||||
},
|
||||
info::plugins::check_mismatched_packages,
|
||||
interface::{rust::get_cargo_target_dir, AppInterface, Interface},
|
||||
ConfigValue, Result,
|
||||
};
|
||||
@ -70,6 +71,11 @@ pub struct Options {
|
||||
/// On subsequent runs, it's recommended to disable this setting again.
|
||||
#[clap(long)]
|
||||
pub skip_stapling: bool,
|
||||
/// Do not error out if a version mismatch is detected on a Tauri package.
|
||||
///
|
||||
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
|
||||
#[clap(long)]
|
||||
pub ignore_version_mismatches: bool,
|
||||
}
|
||||
|
||||
pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
|
||||
@ -131,6 +137,18 @@ pub fn setup(
|
||||
mobile: bool,
|
||||
) -> Result<()> {
|
||||
let tauri_path = tauri_dir();
|
||||
|
||||
// TODO: Maybe optimize this to run in parallel in the future
|
||||
// see https://github.com/tauri-apps/tauri/pull/13993#discussion_r2280697117
|
||||
log::info!("Looking up installed tauri packages to check mismatched versions...");
|
||||
if let Err(error) = check_mismatched_packages(frontend_dir(), tauri_path) {
|
||||
if options.ignore_version_mismatches {
|
||||
log::error!("{error}");
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
|
||||
|
||||
let config_guard = config.lock().unwrap();
|
||||
@ -141,11 +159,9 @@ pub fn setup(
|
||||
.unwrap_or_else(|| "tauri.conf.json".into());
|
||||
|
||||
if config_.identifier == "com.tauri.dev" {
|
||||
log::error!(
|
||||
"You must change the bundle identifier in `{} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
|
||||
bundle_identifier_source
|
||||
anyhow::bail!(
|
||||
"You must change the bundle identifier in `{bundle_identifier_source} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if config_
|
||||
@ -153,12 +169,11 @@ pub fn setup(
|
||||
.chars()
|
||||
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
|
||||
{
|
||||
log::error!(
|
||||
anyhow::bail!(
|
||||
"The bundle identifier \"{}\" set in `{} identifier`. The bundle identifier string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).",
|
||||
config_.identifier,
|
||||
bundle_identifier_source
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if config_.identifier.ends_with(".app") {
|
||||
|
||||
@ -10,6 +10,7 @@ use crate::{
|
||||
get as get_config, reload as reload_config, BeforeDevCommand, ConfigHandle, FrontendDist,
|
||||
},
|
||||
},
|
||||
info::plugins::check_mismatched_packages,
|
||||
interface::{AppInterface, ExitReason, Interface},
|
||||
CommandExt, ConfigValue, Result,
|
||||
};
|
||||
@ -135,6 +136,13 @@ fn command_internal(mut options: Options) -> Result<()> {
|
||||
|
||||
pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHandle) -> Result<()> {
|
||||
let tauri_path = tauri_dir();
|
||||
|
||||
std::thread::spawn(|| {
|
||||
if let Err(error) = check_mismatched_packages(frontend_dir(), tauri_path) {
|
||||
log::error!("{error}");
|
||||
}
|
||||
});
|
||||
|
||||
set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
|
||||
|
||||
if let Some(before_dev) = config
|
||||
|
||||
@ -10,6 +10,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::interface::rust::get_workspace_dir;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct CargoLockPackage {
|
||||
pub name: String,
|
||||
@ -49,6 +51,18 @@ pub struct CargoManifest {
|
||||
pub dependencies: HashMap<String, CargoManifestDependency>,
|
||||
}
|
||||
|
||||
pub fn cargo_manifest_and_lock(tauri_dir: &Path) -> (Option<CargoManifest>, Option<CargoLock>) {
|
||||
let manifest: Option<CargoManifest> = fs::read_to_string(tauri_dir.join("Cargo.toml"))
|
||||
.ok()
|
||||
.and_then(|manifest_contents| toml::from_str(&manifest_contents).ok());
|
||||
|
||||
let lock: Option<CargoLock> = get_workspace_dir()
|
||||
.ok()
|
||||
.and_then(|p| fs::read_to_string(p.join("Cargo.lock")).ok())
|
||||
.and_then(|s| toml::from_str(&s).ok());
|
||||
|
||||
(manifest, lock)
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct CrateVersion {
|
||||
pub version: Option<String>,
|
||||
|
||||
@ -3,9 +3,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::helpers::cross_command;
|
||||
use std::{fmt::Display, path::Path, process::Command};
|
||||
use std::{collections::HashMap, fmt::Display, path::Path, process::Command};
|
||||
|
||||
pub fn manager_version(package_manager: &str) -> Option<String> {
|
||||
cross_command(package_manager)
|
||||
@ -197,6 +198,7 @@ impl PackageManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Use `current_package_versions` as much as possible for better speed
|
||||
pub fn current_package_version<P: AsRef<Path>>(
|
||||
&self,
|
||||
name: &str,
|
||||
@ -254,4 +256,157 @@ impl PackageManager {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_package_versions(
|
||||
&self,
|
||||
packages: &[String],
|
||||
frontend_dir: &Path,
|
||||
) -> crate::Result<HashMap<String, semver::Version>> {
|
||||
let output = match self {
|
||||
PackageManager::Yarn => return yarn_package_versions(packages, frontend_dir),
|
||||
PackageManager::YarnBerry => return yarn_berry_package_versions(packages, frontend_dir),
|
||||
PackageManager::Pnpm => cross_command("pnpm")
|
||||
.arg("list")
|
||||
.args(packages)
|
||||
.args(["--json", "--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()?,
|
||||
// Bun and Deno don't support `list` command
|
||||
PackageManager::Npm | PackageManager::Bun | PackageManager::Deno => cross_command("npm")
|
||||
.arg("list")
|
||||
.args(packages)
|
||||
.args(["--json", "--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()?,
|
||||
};
|
||||
|
||||
let mut versions = HashMap::new();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !output.status.success() {
|
||||
return Ok(versions);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ListOutput {
|
||||
#[serde(default)]
|
||||
dependencies: HashMap<String, ListDependency>,
|
||||
#[serde(default)]
|
||||
dev_dependencies: HashMap<String, ListDependency>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListDependency {
|
||||
version: String,
|
||||
}
|
||||
|
||||
let json: ListOutput = serde_json::from_str(&stdout)?;
|
||||
for (package, dependency) in json.dependencies.into_iter().chain(json.dev_dependencies) {
|
||||
let version = dependency.version;
|
||||
if let Ok(version) = semver::Version::parse(&version) {
|
||||
versions.insert(package, version);
|
||||
} else {
|
||||
log::error!("Failed to parse version `{version}` for NPM package `{package}`");
|
||||
}
|
||||
}
|
||||
Ok(versions)
|
||||
}
|
||||
}
|
||||
|
||||
fn yarn_package_versions(
|
||||
packages: &[String],
|
||||
frontend_dir: &Path,
|
||||
) -> crate::Result<HashMap<String, semver::Version>> {
|
||||
let output = cross_command("yarn")
|
||||
.arg("list")
|
||||
.args(packages)
|
||||
.args(["--json", "--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()?;
|
||||
|
||||
let mut versions = HashMap::new();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !output.status.success() {
|
||||
return Ok(versions);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnListOutput {
|
||||
data: YarnListOutputData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnListOutputData {
|
||||
trees: Vec<YarnListOutputDataTree>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnListOutputDataTree {
|
||||
name: String,
|
||||
}
|
||||
|
||||
for line in stdout.lines() {
|
||||
if let Ok(tree) = serde_json::from_str::<YarnListOutput>(line) {
|
||||
for tree in tree.data.trees {
|
||||
let Some((name, version)) = tree.name.rsplit_once('@') else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(version) = semver::Version::parse(version) {
|
||||
versions.insert(name.to_owned(), version);
|
||||
} else {
|
||||
log::error!("Failed to parse version `{version}` for NPM package `{name}`");
|
||||
}
|
||||
}
|
||||
return Ok(versions);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
fn yarn_berry_package_versions(
|
||||
packages: &[String],
|
||||
frontend_dir: &Path,
|
||||
) -> crate::Result<HashMap<String, semver::Version>> {
|
||||
let output = cross_command("yarn")
|
||||
.args(["info", "--json"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()?;
|
||||
|
||||
let mut versions = HashMap::new();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !output.status.success() {
|
||||
return Ok(versions);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnBerryInfoOutput {
|
||||
value: String,
|
||||
children: YarnBerryInfoOutputChildren,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct YarnBerryInfoOutputChildren {
|
||||
version: String,
|
||||
}
|
||||
|
||||
for line in stdout.lines() {
|
||||
if let Ok(info) = serde_json::from_str::<YarnBerryInfoOutput>(line) {
|
||||
let Some((name, _)) = info.value.rsplit_once('@') else {
|
||||
continue;
|
||||
};
|
||||
if !packages.iter().any(|package| package == name) {
|
||||
continue;
|
||||
}
|
||||
let version = info.children.version;
|
||||
if let Ok(version) = semver::Version::parse(&version) {
|
||||
versions.insert(name.to_owned(), version);
|
||||
} else {
|
||||
log::error!("Failed to parse version `{version}` for NPM package `{name}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ mod env_system;
|
||||
mod ios;
|
||||
mod packages_nodejs;
|
||||
mod packages_rust;
|
||||
mod plugins;
|
||||
pub mod plugins;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsCliVersionMetadata {
|
||||
|
||||
@ -3,14 +3,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{ActionResult, SectionItem};
|
||||
use crate::{
|
||||
helpers::cargo_manifest::{
|
||||
crate_latest_version, crate_version, CargoLock, CargoManifest, CrateVersion,
|
||||
},
|
||||
interface::rust::get_workspace_dir,
|
||||
use crate::helpers::cargo_manifest::{
|
||||
cargo_manifest_and_lock, crate_latest_version, crate_version, CrateVersion,
|
||||
};
|
||||
use colored::Colorize;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn items(frontend_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<SectionItem> {
|
||||
@ -18,17 +14,7 @@ pub fn items(frontend_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<Se
|
||||
|
||||
if tauri_dir.is_some() || frontend_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());
|
||||
|
||||
let (manifest, lock) = cargo_manifest_and_lock(tauri_dir);
|
||||
for dep in ["tauri", "tauri-build", "wry", "tao"] {
|
||||
let crate_version = crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
|
||||
let item = rust_section_item(dep, crate_version);
|
||||
|
||||
@ -3,20 +3,101 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
collections::HashMap,
|
||||
iter,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
self,
|
||||
cargo_manifest::{crate_version, CargoLock, CargoManifest},
|
||||
npm::PackageManager,
|
||||
},
|
||||
interface::rust::get_workspace_dir,
|
||||
use crate::helpers::{
|
||||
self,
|
||||
cargo_manifest::{cargo_manifest_and_lock, crate_version},
|
||||
npm::PackageManager,
|
||||
};
|
||||
|
||||
use super::{packages_nodejs, packages_rust, SectionItem};
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InstalledPackage {
|
||||
pub crate_name: String,
|
||||
pub npm_name: String,
|
||||
pub crate_version: semver::Version,
|
||||
pub npm_version: semver::Version,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InstalledPackages(Vec<InstalledPackage>);
|
||||
|
||||
impl InstalledPackages {
|
||||
pub fn mismatched(&self) -> Vec<&InstalledPackage> {
|
||||
self
|
||||
.0
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.crate_version.major != p.npm_version.major || p.crate_version.minor != p.npm_version.minor
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn installed_tauri_packages(
|
||||
frontend_dir: &Path,
|
||||
tauri_dir: &Path,
|
||||
package_manager: PackageManager,
|
||||
) -> InstalledPackages {
|
||||
let know_plugins = helpers::plugins::known_plugins();
|
||||
let crate_names: Vec<String> = iter::once("tauri".to_owned())
|
||||
.chain(
|
||||
know_plugins
|
||||
.keys()
|
||||
.map(|plugin_name| format!("tauri-plugin-{plugin_name}")),
|
||||
)
|
||||
.collect();
|
||||
let npm_names: Vec<String> = iter::once("@tauri-apps/api".to_owned())
|
||||
.chain(
|
||||
know_plugins
|
||||
.keys()
|
||||
.map(|plugin_name| format!("@tauri-apps/plugin-{plugin_name}")),
|
||||
)
|
||||
.collect();
|
||||
|
||||
let (manifest, lock) = cargo_manifest_and_lock(tauri_dir);
|
||||
|
||||
let mut rust_plugins: HashMap<String, semver::Version> = crate_names
|
||||
.iter()
|
||||
.filter_map(|crate_name| {
|
||||
let crate_version =
|
||||
crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), crate_name).version?;
|
||||
let crate_version = semver::Version::parse(&crate_version)
|
||||
.inspect_err(|_| {
|
||||
log::error!("Failed to parse version `{crate_version}` for crate `{crate_name}`");
|
||||
})
|
||||
.ok()?;
|
||||
Some((crate_name.clone(), crate_version))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut npm_plugins = package_manager
|
||||
.current_package_versions(&npm_names, frontend_dir)
|
||||
.unwrap_or_default();
|
||||
|
||||
let installed_plugins = crate_names
|
||||
.iter()
|
||||
.zip(npm_names.iter())
|
||||
.filter_map(|(crate_name, npm_name)| {
|
||||
let (crate_name, crate_version) = rust_plugins.remove_entry(crate_name)?;
|
||||
let (npm_name, npm_version) = npm_plugins.remove_entry(npm_name)?;
|
||||
Some(InstalledPackage {
|
||||
npm_name,
|
||||
npm_version,
|
||||
crate_name,
|
||||
crate_version,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
InstalledPackages(installed_plugins)
|
||||
}
|
||||
|
||||
pub fn items(
|
||||
frontend_dir: Option<&PathBuf>,
|
||||
@ -27,17 +108,7 @@ pub fn items(
|
||||
|
||||
if tauri_dir.is_some() || frontend_dir.is_some() {
|
||||
if let Some(tauri_dir) = tauri_dir {
|
||||
let manifest: Option<CargoManifest> =
|
||||
if let Ok(manifest_contents) = fs::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| fs::read_to_string(p.join("Cargo.lock")).ok())
|
||||
.and_then(|s| toml::from_str(&s).ok());
|
||||
let (manifest, lock) = cargo_manifest_and_lock(tauri_dir);
|
||||
|
||||
for p in helpers::plugins::known_plugins().keys() {
|
||||
let dep = format!("tauri-plugin-{p}");
|
||||
@ -67,3 +138,28 @@ pub fn items(
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
pub fn check_mismatched_packages(frontend_dir: &Path, tauri_path: &Path) -> crate::Result<()> {
|
||||
let installed_packages = installed_tauri_packages(
|
||||
frontend_dir,
|
||||
tauri_path,
|
||||
PackageManager::from_project(frontend_dir),
|
||||
);
|
||||
let mismatched_packages = installed_packages.mismatched();
|
||||
if mismatched_packages.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mismatched_text = mismatched_packages
|
||||
.iter()
|
||||
.map(
|
||||
|InstalledPackage {
|
||||
crate_name,
|
||||
crate_version,
|
||||
npm_name,
|
||||
npm_version,
|
||||
}| format!("{crate_name} (v{crate_version}) : {npm_name} (v{npm_version})"),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
Err(anyhow!("Found version mismatched Tauri packages. Make sure the NPM and crate versions are on the same major/minor releases:\n{mismatched_text}"))
|
||||
}
|
||||
|
||||
@ -78,6 +78,11 @@ pub struct Options {
|
||||
/// e.g. `tauri android build -- [runnerArgs]`.
|
||||
#[clap(last(true))]
|
||||
pub args: Vec<String>,
|
||||
/// Do not error out if a version mismatch is detected on a Tauri package.
|
||||
///
|
||||
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
|
||||
#[clap(long)]
|
||||
pub ignore_version_mismatches: bool,
|
||||
}
|
||||
|
||||
impl From<Options> for BuildOptions {
|
||||
@ -93,6 +98,7 @@ impl From<Options> for BuildOptions {
|
||||
args: options.args,
|
||||
ci: options.ci,
|
||||
skip_stapling: false,
|
||||
ignore_version_mismatches: options.ignore_version_mismatches,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +88,11 @@ pub struct Options {
|
||||
/// e.g. `tauri ios build -- [runnerArgs]`.
|
||||
#[clap(last(true))]
|
||||
pub args: Vec<String>,
|
||||
/// Do not error out if a version mismatch is detected on a Tauri package.
|
||||
///
|
||||
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
|
||||
#[clap(long)]
|
||||
pub ignore_version_mismatches: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
@ -133,6 +138,7 @@ impl From<Options> for BuildOptions {
|
||||
args: options.args,
|
||||
ci: options.ci,
|
||||
skip_stapling: false,
|
||||
ignore_version_mismatches: options.ignore_version_mismatches,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user