From e48ef539b11d0c8c6f08aaffd6b934ab56fd81a4 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Mar 2023 10:52:50 +0200 Subject: [PATCH 1/4] feat(dtmt): Implement mod migration utility Closes #87. --- CHANGELOG.adoc | 1 + Cargo.lock | 1 + crates/dtmt/Cargo.toml | 1 + crates/dtmt/src/cmd/migrate.rs | 408 +++++++++++++++++++++++++++++++++ crates/dtmt/src/main.rs | 4 + lib/dtmt-shared/src/lib.rs | 22 +- 6 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 crates/dtmt/src/cmd/migrate.rs diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 2a19595..c9e4e70 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -13,6 +13,7 @@ - dtmm: check mod order before deployment - dtmt: add mod dependencies to config - dtmm: match mods to Nexus and check for updates +- dtmt: add utility to migrate mod projects === Fixed diff --git a/Cargo.lock b/Cargo.lock index 16bea81..1e94874 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,6 +849,7 @@ dependencies = [ "futures-util", "glob", "libloading", + "luajit2-sys", "nanorand", "notify", "oodle", diff --git a/crates/dtmt/Cargo.toml b/crates/dtmt/Cargo.toml index 22fba15..524ffaa 100644 --- a/crates/dtmt/Cargo.toml +++ b/crates/dtmt/Cargo.toml @@ -32,6 +32,7 @@ path-clean = "1.0.1" path-slash = "0.2.1" async-recursion = "1.0.2" notify = "5.1.0" +luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" } [dev-dependencies] tempfile = "3.3.0" diff --git a/crates/dtmt/src/cmd/migrate.rs b/crates/dtmt/src/cmd/migrate.rs new file mode 100644 index 0000000..2c5e7f0 --- /dev/null +++ b/crates/dtmt/src/cmd/migrate.rs @@ -0,0 +1,408 @@ +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::path::{Path, PathBuf}; + +use clap::{value_parser, Arg, ArgMatches, Command}; +use color_eyre::eyre::{self, Context}; +use color_eyre::{Help, Report, Result}; +use dtmt_shared::{ModConfig, ModConfigResources, ModDependency}; +use futures::FutureExt; +use luajit2_sys as lua; +use tokio::fs; +use tokio_stream::wrappers::ReadDirStream; +use tokio_stream::StreamExt; + +pub(crate) fn command_definition() -> Command { + Command::new("migrate") + .about("Migrate a mod project from the loose file structure to DTMT.") + .arg( + Arg::new("mod-file") + .required(true) + .value_parser(value_parser!(PathBuf)) + .help("The path to the mod's '.mod' file."), + ) + .arg( + Arg::new("directory") + .required(true) + .value_parser(value_parser!(PathBuf)) + .help( + "The directory to create the mod in. Within this directory, \ + DTMT will create a new folder named after the mod ID and migrate files \ + into that folder.", + ), + ) +} + +#[derive(Clone, Debug)] +struct ModFile { + id: String, + init: PathBuf, + data: Option, + localization: Option, +} + +// This piece of Lua code stubs DMF functions and runs a mod's `.mod` file to extract +// the contained information. +static MOD_FILE_RUNNER: &str = r#" +_DATA = {} + +function fassert() end + +function new_mod(id, options) + _DATA.id = id + _DATA.init = options.mod_script + _DATA.data = options.mod_data + _DATA.localization = options.mod_localization +end + +dmf = { + dofile = function(self, file) + _DATA.init = file + end +} + +_MOD().run() +"#; + +#[tracing::instrument] +async fn evaluate_mod_file(path: impl AsRef + std::fmt::Debug) -> Result { + let path = path.as_ref(); + let code = fs::read(path) + .await + .wrap_err_with(|| format!("Failed to read file '{}'", path.display()))?; + + tokio::task::spawn_blocking(move || unsafe { + let state = lua::luaL_newstate(); + lua::luaL_openlibs(state); + + let code = CString::new(code).expect("Cannot build CString"); + let name = CString::new("_MOD").expect("Cannot build CString"); + + match lua::luaL_loadstring(state, code.as_ptr()) as u32 { + lua::LUA_OK => {} + lua::LUA_ERRSYNTAX => { + let err = lua::lua_tostring(state, -1); + let err = CStr::from_ptr(err).to_string_lossy().to_string(); + + lua::lua_close(state); + + eyre::bail!("Invalid syntax: {}", err); + } + lua::LUA_ERRMEM => { + lua::lua_close(state); + eyre::bail!("Failed to allocate sufficient memory") + } + _ => unreachable!(), + } + + tracing::trace!("Loaded '.mod' code"); + + lua::lua_setglobal(state, name.as_ptr()); + + let code = CString::new(MOD_FILE_RUNNER).expect("Cannot build CString"); + match lua::luaL_loadstring(state, code.as_ptr()) as u32 { + lua::LUA_OK => {} + lua::LUA_ERRSYNTAX => { + let err = lua::lua_tostring(state, -1); + let err = CStr::from_ptr(err).to_string_lossy().to_string(); + + lua::lua_close(state); + + eyre::bail!("Invalid syntax: {}", err); + } + lua::LUA_ERRMEM => { + lua::lua_close(state); + eyre::bail!("Failed to allocate sufficient memory") + } + _ => unreachable!(), + } + + match lua::lua_pcall(state, 0, 1, 0) as u32 { + lua::LUA_OK => {} + lua::LUA_ERRRUN => { + let err = lua::lua_tostring(state, -1); + let err = CStr::from_ptr(err).to_string_lossy().to_string(); + + lua::lua_close(state); + + eyre::bail!("Failed to evaluate '.mod' file: {}", err); + } + lua::LUA_ERRMEM => { + lua::lua_close(state); + eyre::bail!("Failed to allocate sufficient memory") + } + // We don't use an error handler function, so this should be unreachable + lua::LUA_ERRERR => unreachable!(), + _ => unreachable!(), + } + + tracing::trace!("Loaded file runner code"); + + let name = CString::new("_DATA").expect("Cannot build CString"); + lua::lua_getglobal(state, name.as_ptr()); + + let id = { + let name = CString::new("id").expect("Cannot build CString"); + lua::lua_getfield(state, -1, name.as_ptr()); + let val = { + let ptr = lua::lua_tostring(state, -1); + let str = CStr::from_ptr(ptr); + str.to_str() + .expect("ID value is not a valid string") + .to_string() + }; + lua::lua_pop(state, 1); + val + }; + + let path_prefix = format!("{id}/"); + + let init = { + let name = CString::new("init").expect("Cannot build CString"); + lua::lua_getfield(state, -1, name.as_ptr()); + let val = { + let ptr = lua::lua_tostring(state, -1); + let str = CStr::from_ptr(ptr); + str.to_str().expect("ID value is not a valid string") + }; + lua::lua_pop(state, 1); + PathBuf::from(val.strip_prefix(&path_prefix).unwrap_or(val)) + }; + + let data = { + let name = CString::new("data").expect("Cannot build CString"); + lua::lua_getfield(state, -1, name.as_ptr()); + + if lua::lua_isnil(state, -1) > 0 { + None + } else { + let val = { + let ptr = lua::lua_tostring(state, -1); + let str = CStr::from_ptr(ptr); + str.to_str().expect("ID value is not a valid string") + }; + lua::lua_pop(state, 1); + Some(PathBuf::from(val.strip_prefix(&path_prefix).unwrap_or(val))) + } + }; + + let localization = { + let name = CString::new("localization").expect("Cannot build CString"); + lua::lua_getfield(state, -1, name.as_ptr()); + + if lua::lua_isnil(state, -1) > 0 { + None + } else { + let val = { + let ptr = lua::lua_tostring(state, -1); + let str = CStr::from_ptr(ptr); + str.to_str().expect("ID value is not a valid string") + }; + lua::lua_pop(state, 1); + Some(PathBuf::from(val.strip_prefix(&path_prefix).unwrap_or(val))) + } + }; + + lua::lua_close(state); + + let mod_file = ModFile { + id, + init, + data, + localization, + }; + + tracing::trace!(?mod_file); + + Ok(mod_file) + }) + .await + .map_err(Report::new) + .flatten() + .wrap_err("Failed to run mod file handler") +} + +#[async_recursion::async_recursion] +#[tracing::instrument] +async fn process_directory(path: P1, prefix: P2) -> Result<()> +where + P1: AsRef + std::fmt::Debug + std::marker::Send, + P2: AsRef + std::fmt::Debug + std::marker::Send, +{ + let path = path.as_ref(); + let prefix = prefix.as_ref(); + + let read_dir = fs::read_dir(&path) + .await + .wrap_err_with(|| format!("Failed to read directory '{}'", path.display()))?; + + let stream = ReadDirStream::new(read_dir).map(|res| res.wrap_err("Failed to read dir entry")); + tokio::pin!(stream); + + while let Some(res) = stream.next().await { + let entry = res?; + let in_path = entry.path(); + let out_path = prefix.join(entry.file_name()); + + let t = entry.file_type().await?; + + if t.is_dir() { + process_directory(in_path, out_path).await?; + } else { + tracing::trace!( + "Copying file '{}' -> '{}'", + in_path.display(), + out_path.display() + ); + let res = fs::create_dir_all(prefix) + .then(|_| fs::copy(&in_path, &out_path)) + .await + .wrap_err_with(|| { + format!( + "Failed to copy '{}' -> '{}'", + in_path.display(), + out_path.display() + ) + }); + if let Err(err) = res { + tracing::error!("{:?}", err); + } + } + } + + Ok(()) +} + +#[tracing::instrument(skip_all)] +pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { + let (mod_file, in_dir) = { + let path = matches + .get_one::("mod-file") + .expect("Parameter is required"); + + let mod_file = evaluate_mod_file(&path) + .await + .wrap_err("Failed to evaluate '.mod' file")?; + + ( + mod_file, + path.parent().expect("A file path always has a parent"), + ) + }; + + let out_dir = matches + .get_one::("directory") + .expect("Parameter is required"); + + { + let is_dir = fs::metadata(out_dir) + .await + .map(|meta| meta.is_dir()) + .unwrap_or(false); + + if !is_dir { + let err = eyre::eyre!("Invalid output directory '{}'", out_dir.display()); + return Err(err) + .with_suggestion(|| "Make sure the directory exists and is writable.".to_string()); + } + } + + let out_dir = out_dir.join(&mod_file.id); + + println!( + "Enter additional information about your mod '{}'!", + &mod_file.id + ); + + let name = promptly::prompt_default("Display name", mod_file.id.clone()) + .map(|s: String| s.trim().to_string())?; + let summary = promptly::prompt("Short summary").map(|s: String| s.trim().to_string())?; + let author = + promptly::prompt_opt("Author").map(|opt| opt.map(|s: String| s.trim().to_string()))?; + let version = promptly::prompt_default("Version", String::from("0.1.0")) + .map(|s: String| s.trim().to_string())?; + let categories = promptly::prompt("Categories (comma separated list)") + .map(|s: String| s.trim().to_string()) + .map(|s: String| s.split(',').map(|s| s.trim().to_string()).collect())?; + + let packages = vec![PathBuf::from("packages/mods").join(&mod_file.id)]; + + let dtmt_cfg = ModConfig { + dir: out_dir, + id: mod_file.id, + name, + summary, + author, + version, + description: None, + image: None, + categories, + packages, + resources: ModConfigResources { + init: mod_file.init, + data: mod_file.data, + localization: mod_file.localization, + }, + depends: vec![ModDependency::ID(String::from("DMF"))], + }; + + tracing::debug!(?dtmt_cfg); + + fs::create_dir(&dtmt_cfg.dir).await.wrap_err_with(|| { + format!( + "Failed to create mod directory '{}'", + dtmt_cfg.dir.display() + ) + })?; + + tracing::info!("Created mod directory '{}'", dtmt_cfg.dir.display()); + + { + let path = dtmt_cfg.dir.join("dtmt.cfg"); + let data = serde_sjson::to_string(&dtmt_cfg).wrap_err("Failed to serialize dtmt.cfg")?; + fs::write(&path, &data) + .await + .wrap_err_with(|| format!("Failed to write '{}'", path.display()))?; + + tracing::info!("Created mod configuration at '{}'", path.display()); + } + + { + let path = dtmt_cfg + .dir + .join(&dtmt_cfg.packages[0]) + .with_extension("package"); + + let data = { + let mut map = HashMap::new(); + map.insert("lua", vec![format!("scripts/mods/{}/*", dtmt_cfg.id)]); + map + }; + let data = serde_sjson::to_string(&data).wrap_err("Failed to serialize package file")?; + + fs::create_dir_all(path.parent().unwrap()) + .then(|_| fs::write(&path, &data)) + .await + .wrap_err_with(|| format!("Failed to write '{}'", path.display()))?; + + tracing::info!("Created package file at '{}'", path.display()); + } + + { + let path = in_dir.join("scripts"); + let scripts_dir = dtmt_cfg.dir.join("scripts"); + process_directory(&path, &scripts_dir) + .await + .wrap_err_with(|| { + format!( + "Failed to copy files from '{}' to '{}'", + path.display(), + scripts_dir.display() + ) + })?; + + tracing::info!("Copied script files to '{}'", scripts_dir.display()); + } + + Ok(()) +} diff --git a/crates/dtmt/src/main.rs b/crates/dtmt/src/main.rs index 3e1fba2..bd419e7 100644 --- a/crates/dtmt/src/main.rs +++ b/crates/dtmt/src/main.rs @@ -1,5 +1,6 @@ #![feature(io_error_more)] #![feature(let_chains)] +#![feature(result_flattening)] #![windows_subsystem = "console"] use std::path::PathBuf; @@ -19,6 +20,7 @@ mod cmd { pub mod build; pub mod bundle; pub mod dictionary; + pub mod migrate; pub mod murmur; pub mod new; pub mod package; @@ -52,6 +54,7 @@ async fn main() -> Result<()> { .subcommand(cmd::build::command_definition()) .subcommand(cmd::bundle::command_definition()) .subcommand(cmd::dictionary::command_definition()) + .subcommand(cmd::migrate::command_definition()) .subcommand(cmd::murmur::command_definition()) .subcommand(cmd::new::command_definition()) .subcommand(cmd::package::command_definition()) @@ -128,6 +131,7 @@ async fn main() -> Result<()> { Some(("build", sub_matches)) => cmd::build::run(ctx, sub_matches).await?, Some(("bundle", sub_matches)) => cmd::bundle::run(ctx, sub_matches).await?, Some(("dictionary", sub_matches)) => cmd::dictionary::run(ctx, sub_matches).await?, + Some(("migrate", sub_matches)) => cmd::migrate::run(ctx, sub_matches).await?, Some(("murmur", sub_matches)) => cmd::murmur::run(ctx, sub_matches).await?, Some(("new", sub_matches)) => cmd::new::run(ctx, sub_matches).await?, Some(("package", sub_matches)) => cmd::package::run(ctx, sub_matches).await?, diff --git a/lib/dtmt-shared/src/lib.rs b/lib/dtmt-shared/src/lib.rs index d556fdf..bc7f425 100644 --- a/lib/dtmt-shared/src/lib.rs +++ b/lib/dtmt-shared/src/lib.rs @@ -3,46 +3,50 @@ use std::path::PathBuf; mod log; pub use log::*; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use steamlocate::SteamDir; use time::OffsetDateTime; -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct ModConfigResources { pub init: PathBuf, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub data: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub localization: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ModOrder { Before, After, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[serde(untagged)] pub enum ModDependency { ID(String), Config { id: String, order: ModOrder }, } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct ModConfig { #[serde(skip)] pub dir: PathBuf, pub id: String, pub name: String, pub summary: String, - pub description: Option, - pub author: Option, pub version: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub author: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub image: Option, #[serde(default)] pub categories: Vec, + #[serde(default)] pub packages: Vec, pub resources: ModConfigResources, #[serde(default)] -- 2.45.3 From 288adf356ba39a15a150ab885980e610dd2ad598 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Mar 2023 15:31:29 +0200 Subject: [PATCH 2/4] feat(dtmm): Implement resetting dtkit-patch installations Closes #88. --- CHANGELOG.adoc | 1 + crates/dtmm/src/controller/game.rs | 67 +++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c9e4e70..2409280 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -14,6 +14,7 @@ - dtmt: add mod dependencies to config - dtmm: match mods to Nexus and check for updates - dtmt: add utility to migrate mod projects +- dtmm: reset dtkit-patch installations === Fixed diff --git a/crates/dtmm/src/controller/game.rs b/crates/dtmm/src/controller/game.rs index 45ed27e..6f5da47 100644 --- a/crates/dtmm/src/controller/game.rs +++ b/crates/dtmm/src/controller/game.rs @@ -526,11 +526,27 @@ where pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> { let state = Arc::new(state); let bundle_dir = state.game_dir.join("bundle"); + let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())); + + if fs::metadata(bundle_dir.join(format!("{boot_bundle_path}.patch_999"))) + .await + .is_ok() + { + let err = eyre::eyre!("Found dtkit-patch-based mod installation."); + return Err(err) + .with_suggestion(|| { + "If you're a mod author and saved projects directly in 'mods/', \ + use DTMT to migrate them to the new project structure." + .to_string() + }) + .with_suggestion(|| { + "Click 'Reset Game' to remove the previous mod installation.".to_string() + }); + } let (_, game_info, deployment_info) = tokio::try_join!( async { - let path = state.game_dir.join("bundle"); - fs::metadata(&path) + fs::metadata(&bundle_dir) .await .wrap_err("Failed to open game bundle directory") .with_suggestion(|| "Double-check 'Game Directory' in the Settings tab.") @@ -668,6 +684,45 @@ pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> { Ok(()) } +#[tracing::instrument(skip_all)] +async fn reset_dtkit_patch(state: ActionState) -> Result<()> { + let bundle_dir = state.game_dir.join("bundle"); + + { + let path = bundle_dir.join(BUNDLE_DATABASE_NAME); + let backup_path = path.with_extension("data.bak"); + fs::rename(&backup_path, &path).await.wrap_err_with(|| { + format!( + "Failed to move bundle datbase backup '{}' -> '{}'", + backup_path.display(), + path.display() + ) + })?; + tracing::trace!("Reverted bundle database from backup"); + } + + for path in [ + bundle_dir.join(format!( + "{:016x}.patch_999", + Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()) + )), + state.game_dir.join("binaries/mod_loader"), + state.game_dir.join("toggle_darktide_mods.bat"), + state.game_dir.join("README.md"), + ] { + let _ = fs::remove_file(&path).await; + tracing::trace!("Removed file '{}'", path.display()); + } + + for path in [state.game_dir.join("mods"), state.game_dir.join("tools")] { + let _ = fs::remove_dir_all(&path).await; + tracing::trace!("Removed directory '{}'", path.display()); + } + + tracing::info!("Removed dtkit-patch-based mod installation."); + Ok(()) +} + #[tracing::instrument(skip(state))] pub(crate) async fn reset_mod_deployment(state: ActionState) -> Result<()> { let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())); @@ -676,6 +731,14 @@ pub(crate) async fn reset_mod_deployment(state: ActionState) -> Result<()> { tracing::info!("Resetting mod deployment in {}", bundle_dir.display()); + if fs::metadata(bundle_dir.join(format!("{boot_bundle_path}.patch_999"))) + .await + .is_ok() + { + tracing::info!("Found dtkit-patch-based mod installation. Removing."); + return reset_dtkit_patch(state).await; + } + tracing::debug!("Reading mod deployment"); let info: DeploymentData = { -- 2.45.3 From 21d95e492cc104f8e53839fc1c2006de88da7897 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Mar 2023 10:00:14 +0200 Subject: [PATCH 3/4] feat(dtmm): Improve reset logging --- crates/dtmm/src/controller/game.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/dtmm/src/controller/game.rs b/crates/dtmm/src/controller/game.rs index 6f5da47..b5b7f3c 100644 --- a/crates/dtmm/src/controller/game.rs +++ b/crates/dtmm/src/controller/game.rs @@ -1,4 +1,4 @@ -use std::io::{Cursor, ErrorKind}; +use std::io::{self, Cursor, ErrorKind}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; @@ -693,7 +693,7 @@ async fn reset_dtkit_patch(state: ActionState) -> Result<()> { let backup_path = path.with_extension("data.bak"); fs::rename(&backup_path, &path).await.wrap_err_with(|| { format!( - "Failed to move bundle datbase backup '{}' -> '{}'", + "Failed to move bundle database backup '{}' -> '{}'", backup_path.display(), path.display() ) @@ -710,13 +710,23 @@ async fn reset_dtkit_patch(state: ActionState) -> Result<()> { state.game_dir.join("toggle_darktide_mods.bat"), state.game_dir.join("README.md"), ] { - let _ = fs::remove_file(&path).await; - tracing::trace!("Removed file '{}'", path.display()); + match fs::remove_file(&path).await { + Ok(_) => tracing::trace!("Removed file '{}'", path.display()), + Err(err) if err.kind() != io::ErrorKind::NotFound => { + tracing::error!("Failed to remove file '{}': {}", path.display(), err) + } + Err(_) => {} + } } for path in [state.game_dir.join("mods"), state.game_dir.join("tools")] { - let _ = fs::remove_dir_all(&path).await; - tracing::trace!("Removed directory '{}'", path.display()); + match fs::remove_dir_all(&path).await { + Ok(_) => tracing::trace!("Removed directory '{}'", path.display()), + Err(err) if err.kind() != io::ErrorKind::NotFound => { + tracing::error!("Failed to remove directory '{}': {}", path.display(), err) + } + Err(_) => {} + } } tracing::info!("Removed dtkit-patch-based mod installation."); -- 2.45.3 From 2014c3b18787a70585eb923f1717f30e93bcd979 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Mar 2023 20:17:44 +0200 Subject: [PATCH 4/4] feat(dtmt): Fail early in mod migration --- crates/dtmt/src/cmd/migrate.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/dtmt/src/cmd/migrate.rs b/crates/dtmt/src/cmd/migrate.rs index 2c5e7f0..1a4d605 100644 --- a/crates/dtmt/src/cmd/migrate.rs +++ b/crates/dtmt/src/cmd/migrate.rs @@ -309,6 +309,12 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> let out_dir = out_dir.join(&mod_file.id); + fs::create_dir(&out_dir) + .await + .wrap_err_with(|| format!("Failed to create mod directory '{}'", out_dir.display()))?; + + tracing::info!("Created mod directory '{}'", out_dir.display()); + println!( "Enter additional information about your mod '{}'!", &mod_file.id @@ -348,15 +354,6 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> tracing::debug!(?dtmt_cfg); - fs::create_dir(&dtmt_cfg.dir).await.wrap_err_with(|| { - format!( - "Failed to create mod directory '{}'", - dtmt_cfg.dir.display() - ) - })?; - - tracing::info!("Created mod directory '{}'", dtmt_cfg.dir.display()); - { let path = dtmt_cfg.dir.join("dtmt.cfg"); let data = serde_sjson::to_string(&dtmt_cfg).wrap_err("Failed to serialize dtmt.cfg")?; -- 2.45.3