Export function to decomp.me scratch (beta)

This commit is contained in:
Luke Street 2024-01-20 22:53:40 -07:00
parent e88a58ba39
commit c2fcf2797b
12 changed files with 277 additions and 30 deletions

11
Cargo.lock generated
View File

@ -2537,6 +2537,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mime_guess2"
version = "2.0.5"
@ -3403,6 +3413,7 @@ dependencies = [
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"once_cell",
"percent-encoding",

View File

@ -64,12 +64,12 @@ twox-hash = "1.6.3"
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "rustls"] }
reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "multipart", "rustls"] }
self_update = { version = "0.39.0", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = "0.11.23"
reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.39.0"
[target.'cfg(windows)'.dependencies]

View File

@ -16,7 +16,7 @@ use time::UtcOffset;
use crate::{
app_config::{deserialize_config, AppConfigVersion},
config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode},
config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode, ScratchConfig},
diff::DiffAlg,
jobs::{
objdiff::{start_build, ObjDiffConfig},
@ -60,6 +60,7 @@ pub struct ObjectConfig {
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>,
}
#[derive(Clone, Eq, PartialEq)]
@ -128,6 +129,8 @@ pub struct AppConfig {
#[serde(skip)]
pub queue_reload: bool,
#[serde(skip)]
pub queue_scratch: bool,
#[serde(skip)]
pub project_config_info: Option<ProjectConfigInfo>,
}
@ -157,6 +160,7 @@ impl Default for AppConfig {
obj_change: false,
queue_build: false,
queue_reload: false,
queue_scratch: false,
project_config_info: None,
}
}
@ -314,7 +318,7 @@ impl App {
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.config);
diff_state.post_update(&self.config);
diff_state.post_update(ctx, jobs, &self.config);
let Ok(mut config) = self.config.write() else {
return;

View File

@ -60,6 +60,7 @@ impl ObjectConfigV0 {
base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order,
complete: None,
scratch: None,
}
}
}

View File

@ -50,6 +50,22 @@ pub struct ProjectObject {
pub reverse_fn_order: Option<bool>,
#[serde(default)]
pub complete: Option<bool>,
#[serde(default)]
pub scratch: Option<ScratchConfig>,
}
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig {
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
#[serde(default)]
pub c_flags: Option<String>,
#[serde(default)]
pub ctx_path: Option<PathBuf>,
#[serde(default)]
pub build_ctx: bool,
}
impl ProjectObject {
@ -66,7 +82,7 @@ impl ProjectObject {
#[derive(Clone)]
pub enum ProjectObjectNode {
File(String, ProjectObject),
File(String, Box<ProjectObject>),
Dir(String, Vec<ProjectObjectNode>),
}
@ -114,7 +130,7 @@ fn build_nodes(
}
}
}
let mut object = object.clone();
let mut object = Box::new(object.clone());
if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &object.path, &object.target_path)
{

135
src/jobs/create_scratch.rs Normal file
View File

@ -0,0 +1,135 @@
use std::{fs, path::PathBuf, sync::mpsc::Receiver};
use anyhow::{anyhow, bail, Context, Result};
use const_format::formatcp;
use crate::{
app::AppConfig,
jobs::{
objdiff::{run_make, BuildConfig, BuildStatus},
start_job, update_status, Job, JobContext, JobResult, JobState,
},
};
#[derive(Debug, Clone)]
pub struct CreateScratchConfig {
pub build_config: BuildConfig,
pub context_path: Option<PathBuf>,
pub build_context: bool,
// Scratch fields
pub compiler: String,
pub platform: String,
pub compiler_flags: String,
pub function_name: String,
pub target_obj: PathBuf,
}
impl CreateScratchConfig {
pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result<Self> {
let Some(selected_obj) = &config.selected_obj else {
bail!("No object selected");
};
let Some(target_path) = &selected_obj.target_path else {
bail!("No target path for {}", selected_obj.name);
};
let Some(scratch_config) = &selected_obj.scratch else {
bail!("No scratch configuration for {}", selected_obj.name);
};
Ok(Self {
build_config: BuildConfig::from_config(config),
context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx,
compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name,
target_obj: target_path.to_path_buf(),
})
}
pub fn is_available(config: &AppConfig) -> bool {
let Some(selected_obj) = &config.selected_obj else {
return false;
};
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
}
}
#[derive(Default, Debug, Clone)]
pub struct CreateScratchResult {
pub scratch_url: String,
}
#[derive(Debug, Default, Clone, serde::Deserialize)]
struct CreateScratchResponse {
pub slug: String,
pub claim_token: String,
}
const API_HOST: &str = "http://127.0.0.1:8000";
const WEB_HOST: &str = "http://localhost:8080";
fn run_create_scratch(
status: &JobContext,
cancel: Receiver<()>,
config: CreateScratchConfig,
) -> Result<Box<CreateScratchResult>> {
let project_dir =
config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?;
let mut context = None;
if let Some(context_path) = &config.context_path {
if config.build_context {
update_status(status, "Building context".to_string(), 0, 2, &cancel)?;
match run_make(&config.build_config, context_path) {
BuildStatus { success: true, .. } => {}
BuildStatus { success: false, stdout, stderr, .. } => {
bail!("Failed to build context:\n{stdout}\n{stderr}")
}
}
}
let context_path = project_dir.join(context_path);
context = Some(
fs::read_to_string(&context_path)
.map_err(|e| anyhow!("Failed to read {}: {}", context_path.display(), e))?,
);
}
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
let diff_flags = [format!("--disassemble={}", config.function_name)];
let diff_flags = serde_json::to_string(&diff_flags).unwrap();
let obj_path = project_dir.join(&config.target_obj);
let file = reqwest::blocking::multipart::Part::file(&obj_path)
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
let form = reqwest::blocking::multipart::Form::new()
.text("compiler", config.compiler.clone())
.text("platform", config.platform.clone())
.text("compiler_flags", config.compiler_flags.clone())
.text("diff_label", config.function_name.clone())
.text("diff_flags", diff_flags)
.text("context", context.unwrap_or_default())
.text("source_code", "// Move related code from Context tab to here")
.part("target_obj", file);
let client = reqwest::blocking::Client::new();
let response = client
.post(formatcp!("{API_HOST}/api/scratch"))
.multipart(form)
.send()
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
if !response.status().is_success() {
return Err(anyhow!("Failed to create scratch: {}", response.text()?));
}
let body: CreateScratchResponse = response.json().context("Failed to parse response")?;
let scratch_url = format!("{WEB_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token);
update_status(status, "Complete".to_string(), 2, 2, &cancel)?;
Ok(Box::from(CreateScratchResult { scratch_url }))
}
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState {
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| {
run_create_scratch(&context, cancel, config)
.map(|result| JobResult::CreateScratch(Some(result)))
})
}

View File

@ -9,9 +9,13 @@ use std::{
use anyhow::Result;
use crate::jobs::{check_update::CheckUpdateResult, objdiff::ObjDiffResult, update::UpdateResult};
use crate::jobs::{
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
pub mod check_update;
pub mod create_scratch;
pub mod objdiff;
pub mod update;
@ -20,6 +24,7 @@ pub enum Job {
ObjDiff,
CheckUpdate,
Update,
CreateScratch,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
@ -119,6 +124,7 @@ pub enum JobResult {
ObjDiff(Option<Box<ObjDiffResult>>),
CheckUpdate(Option<Box<CheckUpdateResult>>),
Update(Box<UpdateResult>),
CreateScratch(Option<Box<CreateScratchResult>>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {

View File

@ -33,13 +33,28 @@ impl Default for BuildStatus {
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub project_dir: Option<PathBuf>,
pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>,
}
impl BuildConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
Self {
project_dir: config.project_dir.clone(),
custom_make: config.custom_make.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
}
}
}
pub struct ObjDiffConfig {
pub build_config: BuildConfig,
pub build_base: bool,
pub build_target: bool,
pub custom_make: Option<String>,
pub project_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfig>,
pub selected_wsl_distro: Option<String>,
pub code_alg: DiffAlg,
pub data_alg: DiffAlg,
pub relax_reloc_diffs: bool,
@ -47,13 +62,11 @@ pub struct ObjDiffConfig {
impl ObjDiffConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
ObjDiffConfig {
Self {
build_config: BuildConfig::from_config(config),
build_base: config.build_base,
build_target: config.build_target,
custom_make: config.custom_make.clone(),
project_dir: config.project_dir.clone(),
selected_obj: config.selected_obj.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
code_alg: config.code_alg,
data_alg: config.data_alg,
relax_reloc_diffs: config.relax_reloc_diffs,
@ -69,7 +82,14 @@ pub struct ObjDiffResult {
pub time: OffsetDateTime,
}
fn run_make(cwd: &Path, arg: &Path, config: &ObjDiffConfig) -> BuildStatus {
pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
let Some(cwd) = &config.project_dir else {
return BuildStatus {
success: false,
stderr: "Missing project dir".to_string(),
..Default::default()
};
};
match (|| -> Result<BuildStatus> {
let make = config.custom_make.as_deref().unwrap_or("make");
#[cfg(not(windows))]
@ -130,8 +150,11 @@ fn run_build(
config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> {
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let project_dir =
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
let project_dir = config
.build_config
.project_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing project dir"))?;
let target_path_rel = if let Some(target_path) = &obj_config.target_path {
Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
@ -171,7 +194,7 @@ fn run_build(
total,
&cancel,
)?;
run_make(project_dir, target_path_rel, &config)
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
@ -185,7 +208,7 @@ fn run_build(
total,
&cancel,
)?;
run_make(project_dir, base_path_rel, &config)
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
};

View File

@ -93,6 +93,7 @@ impl ConfigViewState {
base_path: Some(path),
reverse_fn_order: None,
complete: None,
scratch: None,
});
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path);
@ -102,6 +103,7 @@ impl ConfigViewState {
base_path: Some(base_path),
reverse_fn_order: None,
complete: None,
scratch: None,
});
}
}
@ -393,6 +395,7 @@ fn display_object(
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order,
complete: object.complete,
scratch: object.scratch.clone(),
});
}
}

View File

@ -192,7 +192,7 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
|ui| {
ui.set_width(column_width);
if ui.button("Back").clicked() {
if ui.button("Back").clicked() {
state.current_view = View::SymbolDiff;
}

View File

@ -586,9 +586,23 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
|ui| {
ui.set_width(column_width);
if ui.button("Back").clicked() {
state.current_view = View::SymbolDiff;
}
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff;
}
ui.separator();
if ui
.add_enabled(
!state.scratch_running && state.scratch_available,
egui::Button::new("📲 decomp.me"),
)
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing")
.clicked()
{
state.queue_scratch = true;
}
});
let demangled = demangle(&selected_symbol.symbol_name, &Default::default());
let name = demangled.as_deref().unwrap_or(&selected_symbol.symbol_name);

View File

@ -1,14 +1,15 @@
use std::mem::take;
use egui::{
text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, ScrollArea, SelectableLabel,
TextEdit, Ui, Vec2, Widget,
text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, OpenUrl, ScrollArea,
SelectableLabel, TextEdit, Ui, Vec2, Widget,
};
use egui_extras::{Size, StripBuilder};
use crate::{
app::AppConfigRef,
jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult},
Job, JobQueue, JobResult,
},
@ -33,12 +34,16 @@ pub enum View {
#[derive(Default)]
pub struct DiffViewState {
pub build: Option<Box<ObjDiffResult>>,
pub scratch: Option<Box<CreateScratchResult>>,
pub current_view: View,
pub symbol_state: SymbolViewState,
pub function_state: FunctionViewState,
pub search: String,
pub queue_build: bool,
pub build_running: bool,
pub scratch_available: bool,
pub queue_scratch: bool,
pub scratch_running: bool,
}
#[derive(Default)]
@ -52,15 +57,19 @@ pub struct SymbolViewState {
impl DiffViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
jobs.results.retain_mut(|result| {
if let JobResult::ObjDiff(result) = result {
jobs.results.retain_mut(|result| match result {
JobResult::ObjDiff(result) => {
self.build = take(result);
false
} else {
true
}
JobResult::CreateScratch(result) => {
self.scratch = take(result);
false
}
_ => true,
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.scratch_running = jobs.is_running(Job::CreateScratch);
self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() {
@ -70,16 +79,41 @@ impl DiffViewState {
self.symbol_state.disable_reverse_fn_order = true;
}
}
self.scratch_available = CreateScratchConfig::is_available(&config);
}
}
pub fn post_update(&mut self, config: &AppConfigRef) {
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
if let Some(result) = take(&mut self.scratch) {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
}
if self.queue_build {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
}
}
if self.queue_scratch {
self.queue_scratch = false;
if let Some(function_name) =
self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone())
{
if let Ok(config) = config.read() {
match CreateScratchConfig::from_config(&config, function_name) {
Ok(config) => {
jobs.push_once(Job::CreateScratch, || {
start_create_scratch(ctx, config)
});
}
Err(err) => {
log::error!("Failed to create scratch config: {err}");
}
}
}
}
}
}
}