diff --git a/services/api/assets/units/README.md b/services/api/assets/units/README.md new file mode 100644 index 0000000..34e3374 --- /dev/null +++ b/services/api/assets/units/README.md @@ -0,0 +1,14 @@ +# The silhouettes a card draws machines as + +The nine `web/src/camo/units/*.png` the camo editor previews on, copied. +MegaMek's art, CC BY-NC-SA 4.0. + +Copied rather than referenced across the tree: this service's image is built +from `services` and `crates` only (see `services/api/Dockerfile`), so an +`include_bytes!` reaching into `web/` compiles in a checkout and fails in the +container. + +`share.rs`'s `sprites_match_the_editors` test compares these against the web +copies whenever those are present, so the two cannot drift unnoticed. If it +fails, copy the web ones across again - that is the side the editor draws +from, and this is the copy. diff --git a/services/api/assets/units/atlas.png b/services/api/assets/units/atlas.png new file mode 100644 index 0000000000000000000000000000000000000000..828ff22db6cf09bdaf6cf7b29d66b162bfa17009 GIT binary patch literal 5719 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 literal 0 HcmV?d00001 diff --git a/services/api/assets/units/chippewa.png b/services/api/assets/units/chippewa.png new file mode 100644 index 0000000000000000000000000000000000000000..290d1663f39e42db0188caef234a42770c1cc977 GIT binary patch literal 4497 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 HDWM4fHc1)c literal 0 HcmV?d00001 diff --git a/services/api/assets/units/elemental.png b/services/api/assets/units/elemental.png new file mode 100644 index 0000000000000000000000000000000000000000..e6f10aa41ad65124675887ba68cb6bd584b98089 GIT binary patch literal 1477 zcmeAS@N?(olHy`uVBq!ia0y~yUFgZf>FlgfP?VpR znUl)EpfRy_qOHea2brVsm%UuI_}(f!ny}C+Qb4qDN|;Vl>!q-as|TN4($BawVaW~) zu63;J_F}8~^v>0RZ|b(J=v!a?<42YFC#=`W1_K>L|9FxSYsX&?TkwYBg7{h_K|X{^}p@ zGWD}!jCQU{coN|ttEFAqtgWqWY;5f8>>L~%93355|5Cr_C&W!ki9GiT16HEY)F*|X=)ojZU2`~?dZEMB~L$&w{2SFT*W zdi9z$Yu2q>w_(GEjT<*^+O%o&=FK~H?AW<;=dNA54jede@ZiCtM~|L9efrFqGw05o zyM6oi!-o%_J$v^2`STYqUc7wy^8NewA3l6IoxR`+0|NtNNswPKgTu2MX&_FLx4R2N z2dk_H0|NtRfk$L91B0G22s2hJwJ&2}U|=ut^mS!_#?B(FCv-iu#*~49L9)a(qQp5r zH#aq}gu%HeHL)Z$MWH;iBtya7(>EZzkx!g~fr-b{#W5t}@Y`vb!M7Y3+LF&QYDI-8 zafk_;yRd#_P|n)xeN_5Go+!VKz{h>Jf8E|4d^<2rbM~6o?8j@KPrKR` z8WUb_>FHZvX}mi!{@3rg(`nX@Os-6mg}p;_=hnSGckO4*!j-0)mp3les-MWhbl>LN z&u)!bs+#j`yezm?SiZhK(V`*hx#0apfp6s>*Df@xODHf6%SfHR+F5aJ2GdcUPk-1Q zMZC_6n&m%So1I zbnR^mNq1;-{JZM1va@)|-Ia`2Urv1JH18FY3ESV*yMBM3zbkZk^0)sh_+;KalM5AJ z^69Ty>Z`!Es{y5*YF5!+0gIgjIai&V;_>XuO26a&tbZHTCAFrM)h~?{`FC z&wZ9!yS~rqb+@8kMds_*A6w%Vf7M*`b?@P8(~oW2x8>ut>mUF7wfhTfv8!oX@b#gv k%3A*U2?i28QGeK&PVR~A3157kfq{X+)78&qol`;+07l1?6aWAK literal 0 HcmV?d00001 diff --git a/services/api/assets/units/panther.png b/services/api/assets/units/panther.png new file mode 100644 index 0000000000000000000000000000000000000000..a6e5f25657f2345212cd74f220198cf2255002e0 GIT binary patch literal 2368 zcmeAS@N?(olHy`uVBq!ia0y~yUFgZf>FlgfP?VpR znUl)EpfRy_qOHea2brVs!CRF?Czc5mo(Pz$Gezr>MyO6x>!q-as|TN4($BawVaW~) zu63;J_F}8~^v>^iX%R@6XVV-c>QBnd;9wG#Ccc$-kF~qaB?=!hmIq~KIS@) zcpuI5^?ts`UnWJ>Q_)U+p2>@*BmFVcW9uHrltet3>8Lz;mO7tM_ffuS)-$H^Os$w@ zv1EnRQ|`#=qUWOLT=nYR`tD@>i}Tsqnd>ggILml@C~yiIWoayC;!cb*4f0u%VSnwY z{i`37pG-O#%BFmrX;pxtW!(q+A9vSA&gk0Z`A_1)@1)!Ej~nvEGp^uX^v_y?^uezVX_Bbquc}i>~XIe+y+`VCoC-32|j$ zU|?cl7ZVecl$4Z~mX?*3m6MZ`mzP&iP*7AR#sM3RaH|{Q&(5l(9qD-)YQ__ z($?12(b3V>)z#C})7RHGFfcGQG&C|YGB!3gF)=YUH8nFcGdDN4u&}VSw6wCavbMIi zv9YnWwY9Uev$wZ*aBy&RbaZlZa&~rhadB~Vb#-%db9Z<5@bK{T^z`!b^7i)j@$vEX z_4V`f^Y`};2nYxa3=9ei3Jwkq2?+@e4Gjwm3l9&Eh=_=cjEstkijIzsiHV7gjg50t*xu8tFNzbXlQ6` zY;0<3YHn_BX=!O~ZEb67Yj1Dw?Ck97>gw+9?&<02?d|RB>+A3DpD^XDh%$+-T-n@D9=g(iTV8OzL3l}X~ zw0QC2B}=%$&)8fpFVx| z?Ai0@&tJTF@$%)%SFc{Zd-v}B`}ZF{eE9hB4nJ z=qZCRW5rVYG6n_)_7YEDSN5lD!h#m;m)D(*XJBBEEOCt}an8@pP0cG|a4t$sEJ;mK zD9{7qLbOz>WL zsfgA1MUdyoi&GN3PkuTdpzfQxZ2w-31077^7d?J=sL#HtGbPAvX3gmhLjM*r{B7R!%FVQ53sdgzi_2s0c``Bt@O6l|ZelQZ)jWGxHe~8HzNb!t`)4?& z{k`=4vJv+ZA-4vlt8Y1tm+o01lp54jR8*|S%ixg4(>ceJnL+W)(~lJ~KjzP`^tn6N z(|E>=IXsIeKk|OBYZ=U#Q2)hUUF(FxgwU*=b+`ZhE%lCkci*)zz50FpqbT?JJWm%- zdXyr*-}n%NL+FZgEBgX}JmbE6@2vUkdo%NOB-#J#cv-BpRL+pDc{1D2efpQnjYk9R z7%pgMPx0yd_>X&f+M&N^Y!?<*%H1;BbYEjW zY!k~;>^S_)A>i$y+2Mg+2s45bB-F@b?Rvb)-9dw?&sNc`Rw`sFQTt*y6xlhFjn#M zm1Eb|{c@YHU%E*ljp=+1-zC1Dg>zprDxLUsDkpns>A9IDWseQ69D3fnSMKr0=mm+3 zW7FeaEQ;S#xma%5#1&%g+)rN#7imd-S+}z><*DiYuDYL^YxZ09DxCNdYWt@oa{r^< z%Y5YK%ztGPvr1X@!b_Qw_nOm`*ZQipA(DQ7o4Z1ZK-|9fdVv)YZli64~q$!#nCdtLGT zq|TtyWl~S|S|`7HSE9RjUP`U?6Pb&9XEpln?K{5ic;7wyCA-*#`EOh1rQF{6?lsfq zei<+4n|1Sct<5ah)1Eiu^S?!(|D7@Yw)WTLs^4cfRPRmBRbQEZ((Y+iXP@_z#dj7T zp3K5tEV}g5->=G_H^-mv-?Z*V__l4^_S(;Qs5!rmIdZGf74g~GS*wqzeEIKjs6FZC z#9N=Q9yv2md51*Iy?OooTx%a-4jr-X#jSFfe$!`njxgN@xNA@k5bG literal 0 HcmV?d00001 diff --git a/services/api/assets/units/roc.png b/services/api/assets/units/roc.png new file mode 100644 index 0000000000000000000000000000000000000000..96704c2d6926326bef8e3053e8b569f04d8a8cbd GIT binary patch literal 1536 zcmeAS@N?(olHy`uVBq!ia0y~yUFgZf>FlgfP?VpR znUl)EpfRy_qOHea2brVs!AJSExZDH^PXv^1RM2{)5vtSFdMPa9>cJq*)c5pR4VDFf`c=Dp|DL?ivxuA5d`ElKz-QRaqvvXWky1Zr^r^Dr3 zV>wRmm0wm}`Pik`@!~MUv6M8)i${`#bdO)Rt*P0|{c+8;=l^7EvyLS-_$$pzZp#$T z7dW4Di|u)B(!{5!Q-92&k{yIY5JJrHw&3NkDTQSRG zMV8f5Zq5GCbJ26QCe6J1{$#wwi5J?O>n_W~x$ zC%=-P1f2+FP(IG&bwOcw-3R+0ch^pxJL}6Nro;t*j~3dwHT1(RCH%lRE9?TXsR$j8@sQ6_c$_KNcK;{5;fR(}7^JYjy(=Ed_T88I+0J`3;(ab;p) zU}9mHmzP&mR8&$@QdU-0RaI3}Q&U%0*U-?=($dn_*4EL{(bd(})6>(}*EcXQFf=qY zGBPqYHa0OaF*P+cGcz+cH@CF3w6?akv9YnWwY9Uev$wZ*aBy&RbaZlZa&d8Sb#--f zb8~lh_wexW^78Wb_V)4d@%8of^Yiof_YVjN2n-Ai3JMAi4h{(k2@MSm3k!>gh=`1g zjEahij*gCriHVDgi;s^_NJvObOiW5jN={BrNl8gfO-)Nn%gxQr%gZY$C@3v0EiW&x zsHmu{tgNc4s;;iCsi~>0t*xu8tFNzbXlQ6|ZfDz7Yinz7Z|~^n=gww0 z>FMw9pEPOGeT7er_Y!%W7e!$vuDqqGiT1+xpU{un>T;{`~?dZELyZ^ z@#4ixmo8nlY}xYV%U7&ev2x|g)vH&pS+i#C+O_M}ty{l-{e}%2Hg4RwdGqFN+qP}r zzJ14z9lLhz+P!=CzJ2=+95`_B;K4(O4jn#x_}H;yr%#_gbLPz1vuDqpJ9qy4`3n~= zT)cSk(xppRu3Wi#_3HKO*Kgmxedo@dyLa#2yLa#Y{rgXzJbC)`>GS8$U%Ytn>eZ{) zuV24=_wN1s_a8rg{PN|?_wV0-{P^+n=g(iie*OOa`_G?0|Ni~^|NsAq!+p^V3=E7V zL4Lsu4$p3+fjCLt?k)@+tg;>q3=EtF9+AZi40_5S%viD1zKnr^fxX1j*OmPVr-*>K zP-PjrG6MsHWQl7;iF1B#Zfaf$gL6@8Vo7R>LV0FMhJw4NZ$Nk>pEv^p<6Tb|$B>A_ z$q5pyixWh8ngR{D*w`{M1MmBkW=p@Ct@ZMus(Y!kt4>bJiad?|6I)HL@;(i^Jb6i# z$>!ouos;vIEt{=t7Q9n-PwGkYkeioWnY#_<=Wno4?PNdcdaYpEg7hUyKhsYAX!TfS zFr(?FTA-Ri;>H6STs)$mxJ1n+m|tfx>V357aqq%IMX6V_o=9s6%u>zEOIJO7Vu5sK z_H|dbng1tr!gBA7QgzcX)xLUu%^n^C976yx~nf=SQ0eb z_xGEbe{KKn}y`rDW9B_&m)XNAwYX1w*-iXAp)%I1p4CM;Ssxj8ocxX0}4 zH_Wx{)~s53$o<&8gQo(6rd|@7IeBJlbK~_hSC3n2U5uU7*xwvHr_rmy{eHypTW(WM wg>tBCOD}(r(VY&7K(~V$60D0GzA-S&=}GMoo0)f>fq{X+)78&qol`;+0OcFR>;M1& literal 0 HcmV?d00001 diff --git a/services/api/assets/units/scorpion.png b/services/api/assets/units/scorpion.png new file mode 100644 index 0000000000000000000000000000000000000000..fccf6eb17c7530e4b3ccfae6d8f2475f154f5703 GIT binary patch literal 1333 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 CXKkzi literal 0 HcmV?d00001 diff --git a/services/api/assets/units/union.png b/services/api/assets/units/union.png new file mode 100644 index 0000000000000000000000000000000000000000..b2f7c2f705d455dffc505958907be1d48d40d9c0 GIT binary patch literal 6759 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 E0IZbHHvj+t literal 0 HcmV?d00001 diff --git a/services/api/src/share.rs b/services/api/src/share.rs index fc39eb6..4dad5ff 100644 --- a/services/api/src/share.rs +++ b/services/api/src/share.rs @@ -385,48 +385,31 @@ const TAGLINE: &str = "placeholder: quillon sarsen wold"; /// The silhouettes a machine is drawn as, and the bytes behind them. /// -/// The nine the camo editor previews on (`web/src/camo/units/`), served from -/// this binary rather than copied anywhere: same repository, same files, one -/// place to change them. They are MegaMek's own art under CC BY-NC-SA 4.0. +/// The nine the camo editor previews on, copied under `assets/` because this +/// service's image is built from `services` and `crates` alone - reaching +/// into `web/` compiles here and fails in the container. `sprites_match_the +/// _editors` keeps the two copies honest. MegaMek's art, CC BY-NC-SA 4.0. /// /// A class of machine rather than a portrait of one - a Panther and a Locust /// are both "light" here - which is why the name is written beside the /// picture. It is the same compromise the lobby makes when a player is /// picking the force, and this screen should look like that one. const SPRITES: [(&str, &[u8]); 9] = [ - ( - "panther", - include_bytes!("../../../web/src/camo/units/panther.png"), - ), - ( - "scorpion", - include_bytes!("../../../web/src/camo/units/scorpion.png"), - ), + ("panther", include_bytes!("../assets/units/panther.png")), + ("scorpion", include_bytes!("../assets/units/scorpion.png")), ( "timberwolf", - include_bytes!("../../../web/src/camo/units/timberwolf.png"), - ), - ( - "atlas", - include_bytes!("../../../web/src/camo/units/atlas.png"), + include_bytes!("../assets/units/timberwolf.png"), ), + ("atlas", include_bytes!("../assets/units/atlas.png")), ( "demolisher", - include_bytes!("../../../web/src/camo/units/demolisher.png"), - ), - ( - "chippewa", - include_bytes!("../../../web/src/camo/units/chippewa.png"), - ), - ( - "elemental", - include_bytes!("../../../web/src/camo/units/elemental.png"), - ), - ("roc", include_bytes!("../../../web/src/camo/units/roc.png")), - ( - "union", - include_bytes!("../../../web/src/camo/units/union.png"), + include_bytes!("../assets/units/demolisher.png"), ), + ("chippewa", include_bytes!("../assets/units/chippewa.png")), + ("elemental", include_bytes!("../assets/units/elemental.png")), + ("roc", include_bytes!("../assets/units/roc.png")), + ("union", include_bytes!("../assets/units/union.png")), ]; /// Which silhouette a machine gets, from its own kind and tonnage. The Mek @@ -2988,6 +2971,30 @@ mod tests { ); } + /// The sprites are a copy of the ones the camo editor draws from, made + /// because the image is built without `web/`. Nothing stops the two + /// drifting except this. + /// + /// Skipped where the web tree is not there - the point is to catch an + /// edit to one copy, and a checkout that has only one copy cannot have + /// made one. + #[test] + fn sprites_match_the_editors() { + let root = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../web/src/camo/units"); + if !root.is_dir() { + return; + } + for (name, bytes) in SPRITES { + let theirs = std::fs::read(root.join(format!("{name}.png"))) + .unwrap_or_else(|e| panic!("{name}.png is embedded but not in web/: {e}")); + assert_eq!( + theirs, bytes, + "{name}.png differs between web/src/camo/units and services/api/assets/units" + ); + } + } + /// The row's status is what separates "not yet" from "not ever". #[test] fn a_live_match_is_unfinished_and_a_dead_one_is_not() { diff --git a/services/api/tests/embedded_assets.rs b/services/api/tests/embedded_assets.rs new file mode 100644 index 0000000..7fe4ec5 --- /dev/null +++ b/services/api/tests/embedded_assets.rs @@ -0,0 +1,98 @@ +//! Everything this binary embeds has to be inside what its image is built +//! from. +//! +//! `services/api/Dockerfile` copies `Cargo.toml`, `Cargo.lock`, `services` +//! and `crates` - and nothing else. An `include_bytes!` reaching outside +//! those compiles in a checkout, where the whole repository is present, and +//! fails in the container with "No such file or directory". That is a broken +//! release found at deploy time rather than at desk time, which is what this +//! is here to prevent. + +use std::path::{Path, PathBuf}; + +/// The directories `services/api/Dockerfile` copies into the build stage. +const COPIED: [&str; 2] = ["services", "crates"]; + +#[test] +fn every_embedded_path_is_inside_the_image_build_context() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let mut checked = 0; + let mut outside = Vec::new(); + + for dir in COPIED { + for source in rust_files(&root.join(dir)) { + // This file names both macros in order to look for them, so it + // would otherwise report itself. + if source.ends_with("tests/embedded_assets.rs") { + continue; + } + let Ok(text) = std::fs::read_to_string(&source) else { + continue; + }; + for asked in embedded_paths(&text) { + checked += 1; + let target = source.parent().unwrap().join(&asked); + let Ok(target) = target.canonicalize() else { + outside.push(format!("{} -> {asked} (missing)", source.display())); + continue; + }; + let inside = COPIED.iter().any(|dir| { + root.join(dir) + .canonicalize() + .is_ok_and(|copied| target.starts_with(copied)) + }); + if !inside { + outside.push(format!("{} -> {asked}", source.display())); + } + } + } + } + + assert!( + outside.is_empty(), + "embedded from outside the image build context ({COPIED:?}):\n {}", + outside.join("\n ") + ); + // A walk that found nothing has passed without checking anything. + assert!(checked > 0, "no embedded assets found - is the walk right?"); +} + +fn rust_files(dir: &Path) -> Vec { + let mut found = Vec::new(); + let Ok(entries) = std::fs::read_dir(dir) else { + return found; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + // Build output, not source, and enormous. + if path.file_name().is_some_and(|name| name == "target") { + continue; + } + found.extend(rust_files(&path)); + } else if path.extension().is_some_and(|ext| ext == "rs") { + found.push(path); + } + } + found +} + +/// The literal path out of every `include_bytes!("...")` and +/// `include_str!("...")`. Text matching rather than parsing: a macro written +/// across two lines or handed a constant is not something this repository +/// does, and a test that misses one of those is better than a syn dependency +/// to catch it. +fn embedded_paths(text: &str) -> Vec { + let mut found = Vec::new(); + for macro_name in ["include_bytes!(\"", "include_str!(\""] { + let mut rest = text; + while let Some(at) = rest.find(macro_name) { + rest = &rest[at + macro_name.len()..]; + if let Some(end) = rest.find('"') { + found.push(rest[..end].to_owned()); + rest = &rest[end..]; + } + } + } + found +}