objdiff_core/jobs/
create_scratch.rs1use std::{fs, sync::mpsc::Receiver, task::Waker};
2
3use anyhow::{Context, Result, anyhow, bail};
4use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf};
5
6use crate::{
7 build::{BuildConfig, BuildStatus, run_make},
8 diff::Demangler,
9 jobs::{Job, JobContext, JobResult, JobState, start_job, update_status},
10};
11
12#[derive(Debug, Clone)]
13pub struct CreateScratchConfig {
14 pub build_config: BuildConfig,
15 pub context_path: Option<Utf8UnixPathBuf>,
16 pub build_context: bool,
17
18 pub compiler: String,
20 pub platform: String,
21 pub compiler_flags: String,
22 pub function_name: String,
23 pub target_obj: Utf8PlatformPathBuf,
24 pub preset_id: Option<u32>,
25 pub demangler: Demangler,
26}
27
28#[derive(Default, Debug, Clone)]
29pub struct CreateScratchResult {
30 pub scratch_url: String,
31}
32
33#[derive(Debug, Default, Clone, serde::Deserialize)]
34struct CreateScratchResponse {
35 pub slug: String,
36 pub claim_token: String,
37}
38
39const API_HOST: &str = "https://decomp.me";
40
41fn run_create_scratch(
42 status: &JobContext,
43 cancel: Receiver<()>,
44 config: CreateScratchConfig,
45) -> Result<Box<CreateScratchResult>> {
46 let project_dir =
47 config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?;
48
49 let mut context = None;
50 if let Some(context_path) = &config.context_path {
51 if config.build_context {
52 update_status(status, "Building context".to_string(), 0, 2, &cancel)?;
53 match run_make(&config.build_config, context_path.as_ref()) {
54 BuildStatus { success: true, .. } => {}
55 BuildStatus { success: false, stdout, stderr, .. } => {
56 bail!("Failed to build context:\n{stdout}\n{stderr}")
57 }
58 }
59 }
60 let context_path = project_dir.join(context_path.with_platform_encoding());
61 context = Some(
62 fs::read_to_string(&context_path)
63 .map_err(|e| anyhow!("Failed to read {}: {}", context_path, e))?,
64 );
65 }
66
67 update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
68 let diff_flags = [format!("--disassemble={}", config.function_name)];
69 let diff_flags = serde_json::to_string(&diff_flags)?;
70 let file = reqwest::blocking::multipart::Part::file(&config.target_obj)
71 .with_context(|| format!("Failed to open {}", config.target_obj))?;
72 let mut form = reqwest::blocking::multipart::Form::new()
73 .text("compiler", config.compiler.clone())
74 .text("platform", config.platform.clone())
75 .text("compiler_flags", config.compiler_flags.clone())
76 .text("diff_label", config.function_name.clone())
77 .text("diff_flags", diff_flags)
78 .text("context", context.unwrap_or_default())
79 .text("source_code", "// Move related code from Context tab to here")
80 .text(
81 "name",
82 config
83 .demangler
84 .demangle(&config.function_name)
85 .unwrap_or_else(|| config.function_name.clone()),
86 );
87 if let Some(preset) = config.preset_id {
88 form = form.text("preset", preset.to_string());
89 }
90 form = form.part("target_obj", file);
91 let client = reqwest::blocking::Client::new();
92 let response = client
93 .post(format!("{API_HOST}/api/scratch"))
94 .multipart(form)
95 .send()
96 .map_err(|e| anyhow!("Failed to send request: {}", e))?;
97 if !response.status().is_success() {
98 return Err(anyhow!("Failed to create scratch: {}", response.text()?));
99 }
100 let body: CreateScratchResponse = response.json().context("Failed to parse response")?;
101 let scratch_url = format!("{API_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token);
102
103 update_status(status, "Complete".to_string(), 2, 2, &cancel)?;
104 Ok(Box::from(CreateScratchResult { scratch_url }))
105}
106
107pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState {
108 start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
109 run_create_scratch(&context, cancel, config)
110 .map(|result| JobResult::CreateScratch(Some(result)))
111 })
112}