1pub mod audio;
13
14use std::collections::BTreeMap;
15use std::env;
16use std::ffi::{OsStr, OsString};
17use std::fs;
18use std::path::{Path, PathBuf};
19use std::process::Command;
20
21use shuyi_assets::bitmap::Bitmap;
22use shuyi_assets::bitmap::dither::{Dither, OneBitSettings, dither};
23use shuyi_assets::image::Image;
24
25use super::disclosure::{FileKind, Shipped};
26use super::{ExportCx, ExportError, ExportReport, Exporter, RunCx, Signing, archive, fresh, paths};
27use crate::app_info;
28use crate::codec;
29use crate::contract::bake::{BakeInput, bake};
30use crate::contract::target::{Level, TargetCheck, TargetProblem, engine_profiles};
31use crate::game::{GameTools, is_unsupported};
32use crate::notices::{self, Build, Section, Text, Work};
33use crate::project::{AppPart, Project};
34
35pub const DEVICE: &str = "thumbv7em-none-eabihf";
37
38pub const SIMULATOR: &str = "x86_64-pc-windows-gnu";
40
41pub const FEATURE: &str = "playdate";
44
45const CFG: &str = "--cfg=shuyi_playdate_sdk";
47
48const DEVICE_RUSTFLAGS: [&str; 4] = [
52 "-Ctarget-cpu=cortex-m7",
53 "-Ctarget-feature=-fp64",
54 "-Crelocation-model=pic",
55 CFG,
56];
57
58const DEVICE_CPU_FLAGS: [&str; 5] = [
61 "-mthumb",
62 "-mcpu=cortex-m7",
63 "-mfloat-abi=hard",
64 "-mfpu=fpv5-sp-d16",
65 "-D__FPU_USED=1",
66];
67
68const DEVICE_COMPILE_FLAGS: [&str; 13] = [
72 "-O2",
73 "-falign-functions=16",
74 "-fomit-frame-pointer",
75 "-Wall",
76 "-Wno-unused",
77 "-Wstrict-prototypes",
78 "-Wno-unknown-pragmas",
79 "-mword-relocations",
80 "-fno-common",
81 "-ffunction-sections",
82 "-fdata-sections",
83 "-DTARGET_PLAYDATE=1",
84 "-DTARGET_EXTENSION=1",
85];
86
87const SHIM: &str = include_str!("../../../playdate/shim/shim.c");
90
91const MSYS2_ARM_GCC: &str = r"C:\msys64\ucrt64\bin\arm-none-eabi-gcc.exe";
94
95const MSYS2_GCC: &str = r"C:\msys64\mingw64\bin\gcc.exe";
98
99const LAUNCHER: &str = "launcher";
101
102const ICON_SIDE: u32 = 32;
104
105const CARD_SIZE: (u32, u32) = (350, 155);
107
108const CARD_ICON_SIDE: u32 = 128;
110
111fn sdk_help(looked: &str) -> String {
113 format!(
114 "the Playdate SDK wasn't found (looked in: {looked}). Read Panic's SDK license at \
115 https://play.date/dev/ and, if you accept it, download the SDK and run its installer \
116 with the defaults. Set PLAYDATE_SDK_PATH if it isn't in ~/Documents/PlaydateSDK"
117 )
118}
119
120fn arm_gcc_help() -> String {
122 format!(
123 "arm-none-eabi-gcc wasn't found on PATH or at {MSYS2_ARM_GCC}. Install it with MSYS2 \
124 (C:\\msys64\\usr\\bin\\pacman.exe -S mingw-w64-ucrt-x86_64-arm-none-eabi-toolchain) or, \
125 on Linux, apt-get install gcc-arm-none-eabi"
126 )
127}
128
129pub fn sdk_candidates(sdk_path: Option<OsString>, home: Option<PathBuf>) -> Vec<PathBuf> {
132 let from_env = sdk_path.filter(|path| !path.is_empty()).map(PathBuf::from);
133 let default = home.map(|home| home.join("Documents").join("PlaydateSDK"));
134 from_env.into_iter().chain(default).collect()
135}
136
137pub fn find_sdk(candidates: &[PathBuf]) -> Result<PathBuf, ExportError> {
140 candidates
141 .iter()
142 .find(|path| path.join("C_API").join("pd_api.h").is_file())
143 .cloned()
144 .ok_or_else(|| {
145 let looked: Vec<String> = candidates.iter().map(|p| p.display().to_string()).collect();
146 let looked = if looked.is_empty() {
147 "nowhere: no PLAYDATE_SDK_PATH and no home folder".to_owned()
148 } else {
149 looked.join(", ")
150 };
151 ExportError::MissingTool(sdk_help(&looked))
152 })
153}
154
155fn sdk_here() -> Result<PathBuf, ExportError> {
157 find_sdk(&sdk_candidates(
158 env::var_os("PLAYDATE_SDK_PATH"),
159 paths::home(),
160 ))
161}
162
163fn find_tool(
166 file_name: &str,
167 first: &[&Path],
168 path_var: Option<&OsStr>,
169 then: &[&Path],
170) -> Option<PathBuf> {
171 first
172 .iter()
173 .map(|path| path.to_path_buf())
174 .chain(
175 path_var
176 .into_iter()
177 .flat_map(env::split_paths)
178 .map(|dir| dir.join(file_name)),
179 )
180 .chain(then.iter().map(|path| path.to_path_buf()))
181 .find(|path| path.is_file())
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct Tools {
187 pub sdk: PathBuf,
189 pub arm_gcc: PathBuf,
191 pub simulator: Result<PathBuf, String>,
193 pub rustflags: (String, String),
195}
196
197fn simulator_here(path_var: Option<&OsStr>) -> Result<PathBuf, String> {
201 if !cfg!(windows) {
202 return Err(
203 "no Simulator part: the export builds one on Windows only, so this .pdx runs on the \
204 device alone"
205 .to_owned(),
206 );
207 }
208 if super::installed(SIMULATOR).is_err() {
209 return Err(format!(
210 "no Simulator part: the Rust target {SIMULATOR} isn't installed (rustup target add \
211 {SIMULATOR})"
212 ));
213 }
214 find_tool("gcc.exe", &[Path::new(MSYS2_GCC)], path_var, &[]).ok_or_else(|| {
215 format!(
216 "no Simulator part: MinGW's gcc wasn't found at {MSYS2_GCC} or on PATH (C:\\msys64\\usr\\bin\\pacman.exe \
217 -S mingw-w64-x86_64-gcc)"
218 )
219 })
220}
221
222fn tools_here(cx: &ExportCx) -> Result<Tools, ExportError> {
224 let sdk = sdk_here()?;
225 super::installed(DEVICE)?;
226 let path_var = env::var_os("PATH");
227 let arm_gcc = find_tool(
228 &format!("arm-none-eabi-gcc{}", env::consts::EXE_SUFFIX),
229 &[],
230 path_var.as_deref(),
231 &[Path::new(MSYS2_ARM_GCC)],
232 )
233 .ok_or_else(|| ExportError::MissingTool(arm_gcc_help()))?;
234 let remaps = if cx.release {
235 let outside = paths::outside(&cx.root, &cx.workspace);
236 paths::remaps(&cx.workspace, &cx.target_dir, &cx.package, &outside)
237 } else {
238 Vec::new()
239 };
240 let flags = paths::remapped_flags_here(&remaps);
241 Ok(Tools {
242 sdk,
243 arm_gcc,
244 simulator: simulator_here(path_var.as_deref()),
245 rustflags: (
246 with_flags(&flags, &DEVICE_RUSTFLAGS),
247 with_flags(&flags, &[CFG]),
248 ),
249 })
250}
251
252fn with_flags(encoded: &str, flags: &[&str]) -> String {
254 let mut all: Vec<&str> = encoded.split('\x1f').filter(|f| !f.is_empty()).collect();
255 all.extend(flags);
256 all.join("\x1f")
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub enum Step {
262 Run {
264 what: &'static str,
266 program: PathBuf,
268 args: Vec<OsString>,
270 envs: Vec<(String, String)>,
272 },
273 Search(PathBuf),
275}
276
277fn os_args<I, S>(items: I) -> Vec<OsString>
279where
280 I: IntoIterator<Item = S>,
281 S: Into<OsString>,
282{
283 items.into_iter().map(Into::into).collect()
284}
285
286fn cargo_args(cx: &ExportCx, triple: &str) -> Vec<String> {
288 let mut args: Vec<String> = [
289 "rustc",
290 "-p",
291 &cx.package,
292 "--lib",
293 "--crate-type",
294 "staticlib",
295 "--no-default-features",
296 "--features",
297 FEATURE,
298 "--target",
299 triple,
300 ]
301 .map(str::to_owned)
302 .to_vec();
303 if cx.release {
304 args.push("--release".to_owned());
305 }
306 args
307}
308
309fn profile_env(release: bool, rustflags: &str) -> Vec<(String, String)> {
315 let settings: &[(&str, &str)] = if release {
316 &[
317 ("CARGO_PROFILE_RELEASE_PANIC", "abort"),
318 ("CARGO_PROFILE_RELEASE_LTO", "fat"),
319 ("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "1"),
320 ("CARGO_PROFILE_RELEASE_STRIP", "false"),
321 ("CARGO_PROFILE_RELEASE_DEBUG", "line-tables-only"),
322 ]
323 } else {
324 &[("CARGO_PROFILE_DEV_PANIC", "abort")]
325 };
326 settings
327 .iter()
328 .map(|&(key, value)| (key.to_owned(), value.to_owned()))
329 .chain([(paths::ENCODED.to_owned(), rustflags.to_owned())])
330 .collect()
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct Dirs {
336 pub source: PathBuf,
338 pub obj: PathBuf,
340 pub build: PathBuf,
342}
343
344impl Dirs {
345 fn of(cx: &ExportCx) -> Self {
347 let build = cx.target_dir.join("playdate").join(&cx.package);
348 Self {
349 source: build.join("Source"),
350 obj: build.join("obj"),
351 build,
352 }
353 }
354}
355
356fn rust_lib(cx: &ExportCx, triple: &str) -> PathBuf {
358 let profile = if cx.release { "release" } else { "debug" };
359 cx.target_dir
360 .join(triple)
361 .join(profile)
362 .join(format!("lib{}.a", cx.package.replace('-', "_")))
363}
364
365fn device_steps(cx: &ExportCx, tools: &Tools, dirs: &Dirs) -> Vec<Step> {
367 let c_api = tools.sdk.join("C_API");
368 let compile = |input: PathBuf, output: PathBuf, strict: bool| {
369 let mut args = os_args(
370 DEVICE_CPU_FLAGS
371 .iter()
372 .chain(&DEVICE_COMPILE_FLAGS)
373 .copied(),
374 );
375 if strict {
376 args.push("-Werror".into());
377 }
378 args.extend(os_args(["-I".into(), c_api.clone().into_os_string()]));
379 args.extend(os_args(["-c".into(), input.into_os_string()]));
380 args.extend(os_args(["-o".into(), output.into_os_string()]));
381 args
382 };
383 let mut link = os_args([
384 dirs.obj.join("shim.o"),
385 dirs.obj.join("setup.o"),
386 rust_lib(cx, DEVICE),
387 ]);
388 link.push("-nostartfiles".into());
389 link.extend(os_args(DEVICE_CPU_FLAGS.iter().copied()));
390 let mut script = OsString::from("-T");
391 script.push(c_api.join("buildsupport").join("link_map.ld"));
392 link.push(script);
393 let mut map = OsString::from("-Wl,-Map=");
394 map.push(dirs.obj.join("pdex.map"));
395 map.push(",--cref,--gc-sections,--no-warn-mismatch,--emit-relocs");
396 link.push(map);
397 let elf = dirs.source.join("pdex.elf");
398 link.extend(os_args(["-o".into(), elf.clone().into_os_string()]));
399 let mut steps = vec![
400 Step::Run {
401 what: "cargo rustc (device)",
402 program: crate::cargo().into(),
403 args: os_args(cargo_args(cx, DEVICE)),
404 envs: profile_env(cx.release, &tools.rustflags.0),
405 },
406 Step::Run {
407 what: "compile shim.c (device)",
408 program: tools.arm_gcc.clone(),
409 args: compile(dirs.obj.join("shim.c"), dirs.obj.join("shim.o"), true),
411 envs: Vec::new(),
412 },
413 Step::Run {
414 what: "compile setup.c (device)",
415 program: tools.arm_gcc.clone(),
416 args: compile(
417 c_api.join("buildsupport").join("setup.c"),
418 dirs.obj.join("setup.o"),
419 false,
420 ),
421 envs: Vec::new(),
422 },
423 Step::Run {
424 what: "link pdex.elf (device)",
425 program: tools.arm_gcc.clone(),
426 args: link,
427 envs: Vec::new(),
428 },
429 ];
430 if !cx.release {
431 steps.push(Step::Run {
438 what: "drop unwind index sections (device)",
439 program: tools
440 .arm_gcc
441 .with_file_name(format!("arm-none-eabi-objcopy{}", env::consts::EXE_SUFFIX)),
442 args: os_args([
443 OsString::from("--remove-section=.ARM.exidx*"),
444 OsString::from("--remove-section=.rel.ARM.exidx*"),
445 elf.clone().into_os_string(),
446 ]),
447 envs: Vec::new(),
448 });
449 }
450 steps.push(Step::Search(elf));
451 steps
452}
453
454fn simulator_steps(cx: &ExportCx, tools: &Tools, gcc: &Path, dirs: &Dirs) -> Vec<Step> {
456 let c_api = tools.sdk.join("C_API");
457 let mut args = os_args([
458 "-shared",
459 "-O2",
460 "-s",
462 "-Wall",
463 "-D_WINDLL",
464 "-DTARGET_SIMULATOR=1",
465 "-DTARGET_EXTENSION=1",
466 "-I",
467 ]);
468 args.push(c_api.clone().into_os_string());
469 args.extend(os_args([
470 dirs.obj.join("shim.c"),
471 c_api.join("buildsupport").join("setup.c"),
472 rust_lib(cx, SIMULATOR),
473 ]));
474 args.extend(os_args([
476 "-static-libgcc",
477 "-Wl,--no-insert-timestamp",
478 "-o",
479 ]));
480 let dll = dirs.source.join("pdex.dll");
481 args.push(dll.clone().into_os_string());
482 vec![
483 Step::Run {
484 what: "cargo rustc (Simulator)",
485 program: crate::cargo().into(),
486 args: os_args(cargo_args(cx, SIMULATOR)),
487 envs: profile_env(cx.release, &tools.rustflags.1),
488 },
489 Step::Run {
490 what: "link pdex.dll (Simulator)",
491 program: gcc.to_path_buf(),
492 args,
493 envs: Vec::new(),
494 },
495 Step::Search(dll),
496 ]
497}
498
499fn pdc_step(tools: &Tools, dirs: &Dirs, pdx: &Path) -> Step {
501 let pdc = tools
502 .sdk
503 .join("bin")
504 .join(format!("pdc{}", env::consts::EXE_SUFFIX));
505 Step::Run {
506 what: "pdc",
507 program: pdc,
508 args: os_args([
509 OsString::from("-sdkpath"),
510 tools.sdk.clone().into_os_string(),
511 dirs.source.clone().into_os_string(),
512 pdx.as_os_str().to_owned(),
513 ]),
514 envs: Vec::new(),
515 }
516}
517
518pub fn plan(cx: &ExportCx, tools: &Tools, dirs: &Dirs, pdx: &Path) -> Vec<Step> {
520 let mut steps = device_steps(cx, tools, dirs);
521 if let Ok(gcc) = &tools.simulator {
522 steps.extend(simulator_steps(cx, tools, gcc, dirs));
523 }
524 steps.push(pdc_step(tools, dirs, pdx));
525 steps
526}
527
528fn execute(cx: &ExportCx, steps: &[Step]) -> Result<(), ExportError> {
530 for step in steps {
531 match step {
532 Step::Run {
533 what,
534 program,
535 args,
536 envs,
537 } => {
538 let status = Command::new(program)
539 .args(args)
540 .envs(envs.iter().map(|(k, v)| (k, v)))
541 .current_dir(&cx.root)
542 .status()
543 .map_err(|e| {
544 ExportError::MissingTool(format!(
545 "{what}: {} doesn't start: {e}",
546 program.display()
547 ))
548 })?;
549 if !status.success() {
550 return Err(ExportError::Build(format!("{what} exited with {status}")));
551 }
552 }
553 Step::Search(file) => cx.search(file)?,
554 }
555 }
556 Ok(())
557}
558
559pub fn pdxinfo(part: &AppPart, version: &str, images: bool) -> String {
562 let image_path = if images {
563 format!("imagePath={LAUNCHER}\n")
564 } else {
565 String::new()
566 };
567 format!(
568 "name={}\nauthor={}\nbundleID={}\nversion={version}\nbuildNumber={}\n{image_path}",
569 part.name, part.publisher, part.id, part.build
570 )
571}
572
573fn one_bit_png(file: &[u8]) -> Result<Vec<u8>, String> {
576 let bitmap = Bitmap::parse(file)?;
577 let (width, height) = (bitmap.width(), bitmap.height());
578 let style = bitmap.style(0).ok_or("no style")?;
579 let stride = usize::from(width).div_ceil(8);
580 let mut rgba = Vec::with_capacity(usize::from(width) * usize::from(height) * 4);
581 for y in 0..usize::from(height) {
582 for x in 0..usize::from(width) {
583 let at = y * stride + x / 8;
584 let bit = |plane: &[u8]| plane[at] >> (7 - (x % 8)) & 1 == 1;
585 let white = bit(style) || bitmap.mask().is_some_and(|mask| !bit(mask));
586 let shade = if white { 255 } else { 0 };
587 rgba.extend_from_slice(&[shade, shade, shade, 255]);
588 }
589 }
590 codec::encode_png(u32::from(width), u32::from(height), &rgba)
591}
592
593fn dithered(side: u32, rgba: Vec<u8>, settings: OneBitSettings) -> Result<Vec<u8>, String> {
595 let image = Image {
596 width: side,
597 height: side,
598 rgba,
599 };
600 one_bit_png(&dither(&image, settings)?)
601}
602
603fn card_from_icon(side: u32, rgba: &[u8]) -> Result<Vec<u8>, String> {
606 let small = app_info::downscale(side, side, rgba, CARD_ICON_SIDE);
607 let (width, height) = CARD_SIZE;
608 let mut out = vec![255; (width * height * 4) as usize];
609 let (left, top) = ((width - CARD_ICON_SIDE) / 2, (height - CARD_ICON_SIDE) / 2);
610 for y in 0..CARD_ICON_SIDE {
611 for x in 0..CARD_ICON_SIDE {
612 let from = ((y * CARD_ICON_SIDE + x) * 4) as usize;
613 let to = (((y + top) * width + x + left) * 4) as usize;
614 let alpha = u32::from(small[from + 3]);
616 for channel in 0..3 {
617 let shade = u32::from(small[from + channel]) * alpha + 255 * (255 - alpha);
618 out[to + channel] = u8::try_from(shade / 255).unwrap_or(u8::MAX);
619 }
620 }
621 }
622 let image = Image {
623 width,
624 height,
625 rgba: out,
626 };
627 one_bit_png(&dither(
628 &image,
629 OneBitSettings::new(Dither::Threshold(128)),
630 )?)
631}
632
633fn launcher_images(part: &AppPart, root: &Path) -> Result<BTreeMap<&'static str, Vec<u8>>, String> {
637 let mut images = BTreeMap::new();
638 let read = |path: &String| fs::read(root.join(path)).map_err(|e| format!("{path}: {e}"));
639 if let Some(path) = &part.playdate_icon {
640 images.insert("icon.png", read(path)?);
641 }
642 if let Some(path) = &part.playdate_card {
643 images.insert("card.png", read(path)?);
644 }
645 if let Some(icon) = app_info::icon(part, root)? {
646 if !images.contains_key("icon.png") {
647 let small = app_info::downscale(icon.side, icon.side, &icon.rgba, ICON_SIDE);
648 images.insert(
649 "icon.png",
650 dithered(ICON_SIDE, small, OneBitSettings::default())?,
651 );
652 }
653 if !images.contains_key("card.png") {
654 images.insert("card.png", card_from_icon(icon.side, &icon.rgba)?);
655 }
656 }
657 Ok(images)
658}
659
660fn setup_c_notice(sdk: &Path) -> Result<Work, ExportError> {
663 let file = sdk.join("C_API").join("buildsupport").join("License.md");
664 let license = fs::read_to_string(&file)
665 .map_err(|e| ExportError::Build(format!("read {}: {e}", file.display())))?;
666 let mut work = Work::new(Section::Crates, "setup.c, of the Playdate SDK");
667 "0BSD".clone_into(&mut work.license);
668 "Panic Inc., https://play.date/dev/".clone_into(&mut work.source);
669 work.texts = vec![Text::new("the SDK's buildsupport/License.md", license)];
670 Ok(work)
671}
672
673const BAKED: &str = "game.pack";
675
676const BAKED_AUDIO: &str = "baked-audio";
679
680fn bake_audio(cx: &ExportCx, game: &dyn GameTools) -> Result<(), ExportError> {
682 let into = cx.out.join(BAKED_AUDIO);
683 if into.exists() {
684 fs::remove_dir_all(&into)
685 .map_err(|e| ExportError::Build(format!("clear {}: {e}", into.display())))?;
686 }
687 let shipped = match game.audio(&cx.profile) {
688 Ok(shipped) => shipped,
689 Err(e) if is_unsupported(&e) => return Ok(()),
690 Err(e) => return Err(ExportError::Setup(format!("the game's audio: {e}"))),
691 };
692 let jobs = audio::jobs(&shipped, &cx.root)
693 .map_err(|e| ExportError::Setup(format!("the game's audio: {e}")))?;
694 fs::create_dir_all(&into)
695 .map_err(|e| ExportError::Build(format!("create {}: {e}", into.display())))?;
696 audio::bake(&jobs, &audio::cache_dir(&Dirs::of(cx).build), &into)
697 .map_err(|e| ExportError::Build(format!("the game's audio: {e}")))
698}
699
700fn place_audio(cx: &ExportCx, dirs: &Dirs) -> Result<bool, ExportError> {
702 let baked = cx.out.join(BAKED_AUDIO);
703 if !baked.is_dir() {
704 return Ok(false);
705 }
706 let names = fs::read_dir(&baked)
707 .map_err(|e| ExportError::Build(format!("read {}: {e}", baked.display())))?;
708 for entry in names.flatten() {
709 let to = dirs.source.join(entry.file_name());
710 fs::rename(entry.path(), &to)
711 .map_err(|e| ExportError::Build(format!("move to {}: {e}", to.display())))?;
712 }
713 fs::remove_dir_all(&baked)
714 .map_err(|e| ExportError::Build(format!("clear {}: {e}", baked.display())))?;
715 Ok(true)
716}
717
718pub static PLAYDATE: PlaydateExporter = PlaydateExporter;
720
721pub struct PlaydateExporter;
723
724impl PlaydateExporter {
725 fn warnings(
728 cx: &ExportCx,
729 pdx_bytes: u64,
730 simulator: &Result<PathBuf, String>,
731 has_audio: bool,
732 ) -> Vec<TargetProblem> {
733 let budget = engine_profiles().ok().and_then(|profiles| {
734 profiles
735 .get(&cx.profile)
736 .and_then(|profile| profile.profile.budgets.package_bytes)
737 });
738 let mut warnings = Vec::new();
739 if let Some(budget) = budget.filter(|&budget| pdx_bytes > budget) {
740 warnings.push(TargetProblem::display(
741 Level::Warning,
742 TargetCheck::Fits,
743 "budgets.package_bytes".to_owned(),
744 format!("the .pdx is {pdx_bytes} bytes, over the profile's {budget}"),
745 ));
746 }
747 if let Err(why) = simulator {
748 warnings.push(TargetProblem::display(
749 Level::Warning,
750 TargetCheck::Fits,
751 "simulator".to_owned(),
752 why.clone(),
753 ));
754 }
755 if !has_audio {
756 warnings.push(TargetProblem::display(
757 Level::Warning,
758 TargetCheck::Fits,
759 "audio".to_owned(),
760 "the game's tools offer no audio (GameTools::audio), so this .pdx is silent"
761 .to_owned(),
762 ));
763 }
764 warnings
765 }
766}
767
768impl Exporter for PlaydateExporter {
769 fn id(&self) -> &'static str {
770 "playdate"
771 }
772
773 fn profile(&self) -> &'static str {
774 "playdate"
775 }
776
777 fn triples(&self) -> &'static [&'static str] {
778 &[DEVICE, SIMULATOR]
779 }
780
781 fn builds_a_program(&self) -> bool {
782 false
783 }
784
785 fn takes_export_part(&self) -> bool {
786 false
787 }
788
789 fn cargo_args(&self, cx: &ExportCx) -> Vec<Vec<String>> {
790 self.triples()
791 .iter()
792 .map(|triple| cargo_args(cx, triple))
793 .collect()
794 }
795
796 fn prepare(
798 &self,
799 cx: &ExportCx,
800 project: &Project,
801 game: Option<&dyn GameTools>,
802 ) -> Result<(), ExportError> {
803 if !cx.features.iter().any(|feature| feature == FEATURE) {
804 return Err(ExportError::Setup(format!(
805 "the package `{}` has no `{FEATURE}` feature, which makes its library the game \
806 on the Playdate (docs/targets/playdate.md)",
807 cx.package
808 )));
809 }
810 let game = game.ok_or_else(|| {
811 ExportError::Setup(format!(
812 "{} has no `game` part: the Playdate's pack is baked by the game's tools",
813 crate::project::FILE
814 ))
815 })?;
816 bake_audio(cx, game)?;
817 let input = BakeInput {
818 target: Some(cx.profile.clone()),
819 out: Some(format!("{}/{BAKED}", cx.shown.replace('\\', "/"))),
820 ..BakeInput::default()
821 };
822 let answer = bake(game, project, &input);
823 if answer.ok {
824 Ok(())
825 } else {
826 let problems: Vec<String> = answer
827 .result
828 .iter()
829 .flat_map(|output| &output.errors)
830 .map(|problem| problem.message.clone())
831 .chain(answer.error.clone())
832 .collect();
833 Err(ExportError::Build(format!(
834 "the pack: {}",
835 problems.join("; ")
836 )))
837 }
838 }
839
840 fn export(&self, cx: &ExportCx) -> Result<ExportReport, ExportError> {
841 for args in self.cargo_args(cx) {
842 super::no_live_link(&args)?;
843 }
844 let part = cx.app.as_ref().ok_or_else(|| {
845 ExportError::Setup(format!(
846 "{} has no `app` part: a .pdx's pdxinfo is written from it",
847 crate::project::FILE
848 ))
849 })?;
850 let tools = tools_here(cx)?;
851 let dirs = Dirs::of(cx);
852 fresh(&dirs.source)?;
853 fresh(&dirs.obj)?;
854 super::write(&dirs.obj.join("shim.c"), SHIM)?;
855 let images = launcher_images(part, &cx.root).map_err(ExportError::Setup)?;
856 let info = pdxinfo(part, &cx.version, !images.is_empty());
857 super::write(&dirs.source.join("pdxinfo"), &info)?;
858 if !images.is_empty() {
859 let folder = dirs.source.join(LAUNCHER);
860 fresh(&folder)?;
861 for (name, bytes) in &images {
862 let file = folder.join(name);
863 fs::write(&file, bytes)
864 .map_err(|e| ExportError::Build(format!("write {}: {e}", file.display())))?;
865 }
866 }
867 let baked = cx.out.join(BAKED);
868 fs::rename(&baked, dirs.source.join(playdate_pack()))
869 .map_err(|e| ExportError::Build(format!("move the pack {}: {e}", baked.display())))?;
870 let has_audio = place_audio(cx, &dirs)?;
871 let mut works = notices::crates(&cx.root, &cx.package, DEVICE, &[FEATURE.to_owned()])
872 .map_err(ExportError::Build)?;
873 works.extend(notices::engine_files(Build::Native, &works));
874 works.push(setup_c_notice(&tools.sdk)?);
875 works.extend(cx.game.iter().cloned());
876 notices::write(&dirs.source, &cx.title, &works).map_err(ExportError::Build)?;
877 let name = format!("{}.pdx", cx.base());
878 let pdx = cx.out.join(&name);
879 if pdx.exists() {
880 fs::remove_dir_all(&pdx)
881 .map_err(|e| ExportError::Build(format!("clear {}: {e}", pdx.display())))?;
882 }
883 execute(cx, &plan(cx, &tools, &dirs, &pdx))?;
884 for part in ["pdex.bin", "pdex.dll"] {
885 let file = pdx.join(part);
886 if file.exists() {
887 cx.search(&file)?;
888 }
889 }
890 let zip = format!("{name}.zip");
892 let entries =
893 archive::entries(&pdx, &format!("{name}/"), &|_| false).map_err(ExportError::Build)?;
894 archive::zip(&cx.out.join(&zip), &entries).map_err(ExportError::Build)?;
895 let size = cx.size(&pdx)?;
896 let warnings = Self::warnings(cx, size.bytes, &tools.simulator, has_audio);
897 let sizes = vec![size, cx.size(&cx.out.join(&zip))?];
898 let mut report = cx.report(&name, sizes, Signing::None, None);
899 report.warnings = warnings;
900 Ok(report)
901 }
902
903 fn shipped(&self, cx: &ExportCx, report: &ExportReport) -> Shipped {
908 let tree = cx.out.join(&report.package);
909 let archive = cx.out.join(format!("{}.zip", report.package));
910 let mut shipped = Shipped::new(tree, Some(archive), format!("{}/", report.package));
911 for file in ["pdex.bin", "pdex.dll"] {
912 shipped.source(file, FileKind::Program, "the built library");
913 }
914 for file in [
915 "pdxinfo",
916 notices::FILE,
917 "launcher/icon.pdi",
918 "launcher/card.pdi",
919 ] {
920 shipped.source(file, FileKind::Written, "written by the export");
921 }
922 shipped.source(BAKED, FileKind::Pack, "the game's bundle, baked");
923 shipped
925 .varying
926 .insert("pdxinfo".to_owned(), "buildtime=".to_owned());
927 let found = archive::entries(&shipped.tree, "", &|_| false).unwrap_or_default();
928 for entry in found {
929 if entry.name.starts_with("audio/") || entry.name.starts_with("music/") {
930 shipped.source(entry.name, FileKind::LooseAsset, "GameTools::audio, baked");
931 }
932 }
933 shipped.inputs.push((
934 "pdex.elf".to_owned(),
935 "what pdc makes pdex.bin from".to_owned(),
936 Dirs::of(cx).source.join("pdex.elf"),
937 ));
938 shipped
939 }
940
941 fn run(&self, cx: &RunCx) -> Result<(), ExportError> {
945 let pdx = cx.out.join(&cx.report.package);
946 if !pdx.join("pdex.dll").is_file() {
947 let why = cx
948 .report
949 .warnings
950 .iter()
951 .find(|warning| warning.setting == "simulator")
952 .map_or("no Simulator part was built", |warning| {
953 warning.message.as_str()
954 });
955 return Err(ExportError::NoRunStep(format!(
956 "{} can't open in the Simulator: {why}",
957 cx.report.package
958 )));
959 }
960 let simulator = sdk_here()?
961 .join("bin")
962 .join(format!("PlaydateSimulator{}", env::consts::EXE_SUFFIX));
963 let status = Command::new(&simulator).arg(&pdx).status().map_err(|e| {
964 ExportError::MissingTool(format!("{} doesn't start: {e}", simulator.display()))
965 })?;
966 if status.success() {
967 Ok(())
968 } else {
969 Err(ExportError::Build(format!(
970 "the Simulator exited with {status}"
971 )))
972 }
973 }
974}
975
976pub const fn playdate_pack() -> &'static str {
979 BAKED
980}
981
982#[cfg(test)]
983mod tests;