Compare commits
26 commits
Author | SHA1 | Date | |
---|---|---|---|
3d8ae03a2a | |||
ee4b17e1ca | |||
ed2d4bd2b3 | |||
d4ed662cdf | |||
8c16278ffc | |||
3b2c0dd92d | |||
25b701e10a | |||
1fe0245e1f | |||
3395f3edac | |||
8b7103180c | |||
4e2eeb415f | |||
b16ca174ca | |||
4451d995d4 | |||
531f2fec35 | |||
4b12ad7fef | |||
ab0d9faafe | |||
fe65d4982d | |||
35ad9543a9 | |||
0e5ed74517 | |||
e0209f1456 | |||
0a4f1d2a45 | |||
e00bb7868c | |||
7f389d2055 | |||
0504ad56a3 | |||
ae8277de63 | |||
13a14637e3 |
|
@ -43,7 +43,7 @@ data_sources:
|
|||
# same as the items root, but applies to layouts rather than items.
|
||||
layouts_root: /
|
||||
|
||||
base_url: 'http://www.wezm.net'
|
||||
base_url: 'https://www.wezm.net'
|
||||
deploy:
|
||||
default:
|
||||
kind: rsync
|
||||
|
|
|
@ -23,7 +23,7 @@ Find me on the Internet in one of these places:
|
|||
Aggregator of content related to the <a href="https://www.rust-lang.org/">Rust</a> programming language</li>
|
||||
<li><a href="http://www.flickr.com/photos/wezm/">Flickr</a></li>
|
||||
<li><a href="https://github.com/wezm">GitHub</a></li>
|
||||
<li><a href="https://decentralised.social/wezm">Mastodon/Fediverse</a></li>
|
||||
<li><a href="https://mastodon.decentralised.social/@wezm">Mastodon/Fediverse</a></li>
|
||||
<li><a href="https://patreon.com/wezm">Patreon</a></li>
|
||||
<li><a href="http://stackoverflow.com/users/38820/wes">Stack Overflow</a></li>
|
||||
<li><a href="https://twitter.com/wezm">Twitter</a></li>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
User-Agent: *
|
||||
Disallow:
|
||||
Sitemap: <%= @config[:base_url] %>/sitemap.xml
|
||||
Sitemap: <%= @config[:base_url] %>/v2/sitemap.xml
|
||||
|
|
|
@ -114,7 +114,7 @@ from.
|
|||
* [Jeremy Soller] is creating [Redox OS], an MIT licensed OS written in Rust.
|
||||
$60
|
||||
* [Gargron] is creating the [Mastodon] federated social network. You can find
|
||||
me at <https://decentralised.social/wezm>. $40
|
||||
me at <https://mastodon.decentralised.social/@wezm>. $40
|
||||
* [Jorge Aparicio] is doing fantastic work building Rust tooling and pushing
|
||||
the state of the art of [Rust on microcontrollers][embedded-rust]. $35
|
||||
* [Steve Wills] does a stack of work on [FreeBSD ports/packages][swills-ports].
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<path d="M2 4 C6 8 10 12 15 11 A6 6 0 0 1 22 4 A6 6 0 0 1 26 6 A8 8 0 0 0 31 4 A8 8 0 0 1 28 8 A8 8 0 0 0 32 7 A8 8 0 0 1 28 11 A18 18 0 0 1 10 30 A18 18 0 0 1 0 27 A12 12 0 0 0 8 24 A8 8 0 0 1 3 20 A8 8 0 0 0 6 19.5 A8 8 0 0 1 0 12 A8 8 0 0 0 3 13 A8 8 0 0 1 2 4"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://decentralised.social/wezm" rel="me" class="no-border">
|
||||
<a href="https://mastodon.decentralised.social/@wezm" rel="me" class="no-border">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 216.4144 232.00976" class="social-icon"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="currentColor"></path><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<img src="/images/comment.svg" width="32" height="32" alt="Comment icon" class="comment align-top" />
|
||||
Stay in touch!
|
||||
</h2>
|
||||
<p>Follow me on <a href="https://twitter.com/wezm">Twitter</a> or <a href="https://decentralised.social/wezm">Mastodon</a>, <a href="/feed/">subscribe to the feed</a>, or <a href="mailto:wes@wezm.net">send me an email</a>.
|
||||
<p>Follow me on <a href="https://twitter.com/wezm">Twitter</a> or <a href="https://mastodon.decentralised.social/@wezm">Mastodon</a>, <a href="/feed/">subscribe to the feed</a>, or <a href="mailto:wes@wezm.net">send me an email</a>.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
@ -8,3 +8,7 @@ deploy: build
|
|||
# build with zola
|
||||
build:
|
||||
zola build
|
||||
|
||||
# start zola dev server
|
||||
serve:
|
||||
zola serve
|
||||
|
|
|
@ -8,8 +8,7 @@ body_class = "home"
|
|||
Hi I'm Wes 👋. I like warm weather and tinkering with computers; ranging from
|
||||
small microcontrollers, up to large servers and the operating systems that run
|
||||
upon them. I'm a [Rustacean] {{ quiet(text="🦀") }} with a fondness for
|
||||
mechanical keyboards. I work at [YesLogic] on the [Prince] HTML to PDF
|
||||
converter. [Read more on the about page →](@/about.md)
|
||||
mechanical keyboards. [Read more on the about page →](@/about.md)
|
||||
|
||||
[Prince]: https://www.princexml.com/
|
||||
[Rustacean]: https://www.rust-lang.org/learn/get-started#ferris
|
||||
|
|
|
@ -22,11 +22,6 @@ Ninja 2, which has had the factory controller replaced with a programmable one.
|
|||
community and curate [Read Rust], a website that collects interesting posts
|
||||
from the Rust community.
|
||||
|
||||
I work at [YesLogic] on the [Prince] HTML to PDF converter. Prince is a very
|
||||
interesting product to work on as it is essentially a web-browser that outputs
|
||||
well typeset PDFs. It is largely implemented in the [Mercury] logic programming
|
||||
language, with portions now in [Rust] as well.
|
||||
|
||||
I am fine with either he/him or they/them pronouns.
|
||||
|
||||
[Alpine Linux]: https://alpinelinux.org/
|
||||
|
@ -37,7 +32,6 @@ I am fine with either he/him or they/them pronouns.
|
|||
[my desktop]: https://bitcannon.net/page/ryzen9-pc/
|
||||
[my laptop]: https://bitcannon.net/post/huawei-matebook-x-pro-void-linux/
|
||||
[my server]: https://www.wezm.net/technical/2019/02/alpine-linux-docker-infrastructure/
|
||||
[Prince]: https://www.princexml.com/
|
||||
[Read Rust]: https://readrust.net/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[Void Linux]: https://voidlinux.org/
|
||||
|
|
|
@ -6,7 +6,7 @@ date = 2021-08-26T09:10:54+10:00
|
|||
#updated = 2021-05-15T10:15:08+10:00
|
||||
+++
|
||||
|
||||
On 24 August I received an email from Vultr saying that my server had used 78%
|
||||
On 24 August I received an email from [Vultr] saying that my server had used 78%
|
||||
of its 3Tb bandwidth allocation for the month. This was surprising as last time
|
||||
I looked I only used a small fraction of this allocation across the various
|
||||
[things I host][alpine-docker].
|
||||
|
@ -75,3 +75,4 @@ my peace with linking to Twitter.
|
|||
[nitter-instance]: https://decentralised.social/notice/A41E2cjuM14UYFAF7o
|
||||
[robots.txt]: https://github.com/wezm/nitter/commit/4e7bd7b8853bf36008a3d1e79ee97deaa68743da
|
||||
[Varnish]: https://varnish-cache.org/
|
||||
[Vultr]: https://www.vultr.com/?ref=7903263
|
||||
|
|
51
v2/content/posts/2022/garage-door-monitor-update/index.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
+++
|
||||
title = "Garage Door Monitor Update"
|
||||
date = 2022-10-17T15:04:44+10:00
|
||||
|
||||
#[extra]
|
||||
#updated = 2022-04-21T09:07:57+10:00
|
||||
+++
|
||||
|
||||
|
||||
[The garage door monitor that I built earlier in the year](@/posts/2022/garage-door-monitor/index.md)
|
||||
has by all accounts been running perfectly since I installed it. Recently I
|
||||
implemented a couple of new features that I've wished for over the last few
|
||||
months.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
The first new feature is a bit more visibility into the state of the system. I
|
||||
added uptime and memory info to the web page:
|
||||
|
||||
{{ figure(image="posts/2022/garage-door-monitor-update/webpage.png", link="posts/2022/garage-door-monitor-update/webpage.png", alt="Screenshot of the web page served by the garage door monitor. It shows the state of the door, uptime of the device, and memory usage information.", caption="Screenshot of the web page now including uptime and memory info.", width=625) }}
|
||||
|
||||
The second and more practical addition is a subsequent notification when the
|
||||
door is closed again after it was left open for longer that the trigger time (5
|
||||
minutes).
|
||||
|
||||
{{ figure(image="posts/2022/garage-door-monitor-update/new-mattermost-notification.png", link="posts/2022/garage-door-monitor-update/new-mattermost-notification.png", alt="Screenshot of the message posted to Mattermost by the garage door monitor. It reads: Garage door closed after 5 minutes open.", caption="The new notification", width=403, border=true) }}
|
||||
|
||||
Since I set up the garage door monitor I haven't accidentally left it open.
|
||||
However, I have triggered the notification when washing the car or packing for
|
||||
a road trip. It's this latter scenario that I wanted to address. The issue is
|
||||
that because the notification is triggered during packing if I did set off and
|
||||
forget to close the door I wouldn't know. One way to deal with this would be
|
||||
recurring notifications. I chose another option though.
|
||||
|
||||
When the door is closed after notifying that it was left open, it sends another
|
||||
message saying that it's now closed. The benefit of this approach is that the
|
||||
message log now properly tracks the state of the door, so if I'm a few kms down
|
||||
the road and wonder if I closed the door it's just a matter of checking the
|
||||
channel in Mattermost.
|
||||
|
||||
Before I powered it off to update the Buildroot image on the SD card I hooked
|
||||
up a serial console and checked its uptime. I was pleased to see 100% uptime
|
||||
since it was first installed:
|
||||
|
||||
```
|
||||
02:03:14 up 183 days, 20:26, load average: 0.00, 0.00, 0.00
|
||||
```
|
||||
|
||||
That's all for now, on to the next few hundred days of service!
|
||||
|
||||
[Garage door monitor source on GitHub](https://github.com/wezm/garage-door-monitor)
|
After Width: | Height: | Size: 38 KiB |
BIN
v2/content/posts/2022/garage-door-monitor-update/webpage.png
Normal file
After Width: | Height: | Size: 62 KiB |
|
@ -3,7 +3,7 @@ title = "Monitoring My Garage Door With a Raspberry Pi, Rust, and a 13Mb Linux S
|
|||
date = 2022-04-20T06:38:27+10:00
|
||||
|
||||
[extra]
|
||||
updated = 2022-04-21T09:07:57+10:00
|
||||
updated = 2022-10-17T15:51:20+10:00
|
||||
+++
|
||||
|
||||
I've accidentally left our garage door open a few times. To combat this I built
|
||||
|
@ -301,6 +301,9 @@ happens.
|
|||
All the code, hardware designs, and Buildroot configuration/overlay is
|
||||
available in [the git repository][repo] if you're interested.
|
||||
|
||||
[See part two](@/posts/2022/garage-door-monitor-update/index.md) for a couple of new
|
||||
features I added a few months later.
|
||||
|
||||
[app]: https://github.com/wezm/garage-door-monitor/tree/main/app
|
||||
[Awesome]: https://github.com/awesomeWM/awesome
|
||||
[FreeCAD]: https://www.freecadweb.org/
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
title = "Fixing Monospace Text in Kobo eReaders"
|
||||
date = 2022-04-10T09:14:50+10:00
|
||||
|
||||
# [extra]
|
||||
# updated = 2022-01-27T21:07:32+10:00
|
||||
[extra]
|
||||
updated = 2022-08-22T14:10:50+10:00
|
||||
+++
|
||||
|
||||
After verifying with friends that eBook readers do a decent job of rendering
|
||||
|
@ -14,6 +14,11 @@ text with CSS rules like `font-family: monospace` in a monospace font.
|
|||
|
||||
<!-- more -->
|
||||
|
||||
**Update Aug 2022:** I was told and have confirmed myself that the workaround
|
||||
described in this post no longer works with the latest firmware. I tested
|
||||
version 4.33.19759. If anyone knows how to restore the behaviour please get in
|
||||
touch.
|
||||
|
||||
[Skip to Instructions ⮷](#instructions)
|
||||
|
||||
I quickly discovered this is a known issue with Kobo readers dating back years:
|
||||
|
|
6
v2/content/posts/2023/_index.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
+++
|
||||
title = "2023"
|
||||
sort_by = "date"
|
||||
paginate_by = 5
|
||||
transparent = true
|
||||
+++
|
39
v2/content/posts/2023/anz-youtube-makers.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
+++
|
||||
title = "Australian and New Zealand Makers on YouTube"
|
||||
date = 2023-04-11T10:04:27+10:00
|
||||
|
||||
[extra]
|
||||
updated = 2023-04-14T12:16:17+10:00
|
||||
+++
|
||||
|
||||
I decided I wanted to add some more local folks into my YouTube subscriptions.
|
||||
I [put the call out on Mastodon][toot] for suggestions for folks doing videos
|
||||
about machining, woodworking, electronics, software, that type of thing. I
|
||||
received a number of helpful replies and thought it might be useful to collect
|
||||
the list (as well as ones I'm already subscribed to) on this page in case
|
||||
others are looking for new channels to check out.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
* [Andrew Newton](https://www.youtube.com/@AndrewNewton)
|
||||
* [Artisan Makes](https://www.youtube.com/@artisanmakes)
|
||||
* [BensWorx](https://www.youtube.com/@BensWorx)
|
||||
* [Clickspring](https://www.youtube.com/@Clickspring)
|
||||
* [Cutting Edge Engineering Australia](https://www.youtube.com/@CuttingEdgeEngineering)
|
||||
* [EEVblog](https://www.youtube.com/@EEVblog)
|
||||
* [iforce2d](https://www.youtube.com/@iforce2d)
|
||||
* [Luke Towan](https://www.youtube.com/@LukeTowan)
|
||||
* [Mach Super](https://www.youtube.com/@machsuper)
|
||||
* [Maker's Muse](https://www.youtube.com/@MakersMuse)
|
||||
* [MattKC](https://www.youtube.com/@MattKC) (Matt moved to the US but he still counts 🙂)
|
||||
* [MooreTech](https://www.youtube.com/@MooreTech) -- It's me!
|
||||
* [Pask Makes](https://www.youtube.com/@PaskMakes)
|
||||
* [Primitive Technology](https://www.youtube.com/@primitivetechnology9550)
|
||||
* [Psivewri](https://www.youtube.com/@psivewri)
|
||||
* [SuperHouseTV](https://www.youtube.com/@SuperHouseTV)
|
||||
* [Unexpected Maker](https://www.youtube.com/@UnexpectedMaker)
|
||||
|
||||
That's the list for now. If you have other suggestions (especially if they can
|
||||
mix up the diversity of the list a bit) please let me know.
|
||||
|
||||
[toot]: https://mastodon.decentralised.social/@wezm/110165869937514088
|
BIN
v2/content/posts/2023/derez/DITL.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
v2/content/posts/2023/derez/Dialog.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
v2/content/posts/2023/derez/MPW.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
182
v2/content/posts/2023/derez/index.md
Normal file
|
@ -0,0 +1,182 @@
|
|||
+++
|
||||
title = "How to use DeRez"
|
||||
date = 2023-03-20T12:44:38+10:00
|
||||
|
||||
#[extra]
|
||||
#updated = 2023-03-01T21:54:06+10:00
|
||||
+++
|
||||
|
||||
After my post on trying to run
|
||||
[Rust on Classic Mac OS post](@/posts/2023/rust-on-ppc-classic-mac-os/index.md) I continued trying to
|
||||
find a modern language that I can use to build classic Mac OS software. I've
|
||||
had some success with [Nim] and built a little
|
||||
[temperature converter application][nim-temp-src]. As part of this I wanted to be able to use
|
||||
[ResEdit] to edit the layout of the dialog. The problem was that I need a way
|
||||
to convert the modified resources back into the textual representation used in
|
||||
the source code. In this post I describe how I did this with `DeRez`.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
{{ video(video="posts/2023/derez/mac-nim-temp.mp4", height=227, poster="png", preload="auto", alt="Video of the temperature converter application.", caption="Video of the temperature converter application.") }}
|
||||
|
||||
To build the temperature converter I started with the [Dialog sample] from
|
||||
[Retro68], which looks like this:
|
||||
|
||||
{{ figure(image="posts/2023/derez/Dialog.png", link="posts/2023/derez/Dialog.png", pixelated=true, alt="Screenshot of the Dialog sample from Retro68. It has a static text item, edit text item, check box, two radio buttons, and a Quit button.", caption="Dialog Sample") }}
|
||||
|
||||
I opened it up in ResEdit and edited the `DITL` (Dialog Template) resource to
|
||||
add the icon and temperature fields. I also added a new `ICON` resource and
|
||||
drew a little thermometer:
|
||||
|
||||
{{ figure(image="posts/2023/derez/DITL.png", link="posts/2023/derez/DITL.png", pixelated=true, alt="Screenshot of the ResEdit DITL editor editing the DITL resource for my temperature converter.", caption="Editing DITL resource in ResEdit") }}
|
||||
|
||||
With the changes made, I now wanted to convert the binary resources stored in
|
||||
the resource fork back into the [textual format used in the source code][dialog.r].
|
||||
I believe the format is called `Rez`, here's a snippet of it:
|
||||
|
||||
```
|
||||
resource 'DITL' (128) {
|
||||
{
|
||||
{ 190-10-20, 320-10-80, 190-10, 320-10 },
|
||||
Button { enabled, "Quit" };
|
||||
|
||||
{ 190-10-20-5, 320-10-80-5, 190-10+5, 320-10+5 },
|
||||
UserItem { enabled };
|
||||
|
||||
{ 10, 10, 30, 310 },
|
||||
StaticText { enabled, "Static Text Item" };
|
||||
|
||||
{ 40, 10, 56, 310 },
|
||||
EditText { enabled, "Edit Text Item" };
|
||||
|
||||
{ 70, 10, 86, 310 },
|
||||
CheckBox { enabled, "Check Box" };
|
||||
|
||||
{ 90, 10, 106, 310 },
|
||||
RadioButton { enabled, "Radio 1" };
|
||||
|
||||
{ 110, 10, 126, 310 },
|
||||
RadioButton { enabled, "Radio 2" };
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
This turned out to be a bit of journey and the motivation for this blog post.
|
||||
As part of the [Macintosh Programmers Workshop][mpw] (MPW) theres is a tool
|
||||
called `DeRez` that does what I want. First up I had to work out how to operate
|
||||
MPW. It's an editable shell where you run commands with ⌘-Return. Once I worked
|
||||
that out I could run `DeRez` on my edited application but I only got the
|
||||
fullback hexadecimal representation of the resources, not the structured output
|
||||
I wanted:
|
||||
|
||||
```
|
||||
data 'DITL' (128) {
|
||||
$"0007 0000 0000 00A0 00E6 00B4 0136 0404" /* ....... .æ.´.6.. */
|
||||
$"5175 6974 0000 0000 009B 00E1 00B9 013B" /* Quit......á.¹.; */
|
||||
$"0000 0000 0000 0046 000A 005A 0136 0808" /* .......F...Z.6.. */
|
||||
$"4865 6C6C 6F20 5E30 0000 0000 001E 000A" /* Hello ^0........ */
|
||||
$"003E 002A A002 0597 0000 0000 0014 0032" /* .>.* .........2 */
|
||||
$"0024 007D 8807 4365 6C73 6975 7300 0000" /* .$.}.Celsius... */
|
||||
$"0000 0014 00AA 0024 00F5 8809 4661 7265" /* .....ª.$.õÆFare */
|
||||
$"6E68 6569 7400 0000 0000 0029 0036 0039" /* nheit......).6.9 */
|
||||
$"0081 1009 4564 6974 2054 6578 7400 0000" /* ..ÆEdit Text... */
|
||||
$"0000 002B 00AE 003B 00F9 1009 4564 6974" /* ...+.®.;.ù.ÆEdit */
|
||||
$"2054 6578 7400" /* Text. */
|
||||
};
|
||||
```
|
||||
|
||||
`Help DeRez` in MPW didn't shed much light on the problem but after a lot of
|
||||
searching online I eventually found some extra details in the [man page for
|
||||
`DeRez`][DeRez man] shipped on Mac OS X. Specifically:
|
||||
|
||||
> The type declarations for the standard
|
||||
> Macintosh resources are contained in the `Carbon.r` resource header file,
|
||||
> contained in the Carbon framework. You may use the ${RIncludes} shell
|
||||
> environment variable to define a default path to resource header files.
|
||||
> If you do not specify any type declaration files, `DeRez` produces data
|
||||
> statements in hexadecimal form.
|
||||
|
||||
and
|
||||
|
||||
> You can also specify resource description
|
||||
> files containing type declarations. For each type declaration file on
|
||||
> the command line, DeRez applies the following search rules:
|
||||
>
|
||||
> 1. DeRez tries to open the file with the name specified as is.
|
||||
>
|
||||
> 2. If rule 1 fails and the filename contains no colons or begins with a
|
||||
> colon, DeRez appends the filename to each of the pathnames specified by
|
||||
> the {RIncludes} environment variable and tries to open the file.
|
||||
|
||||
With this information I was able to construct a command that worked:
|
||||
|
||||
|
||||
```
|
||||
DeRez -i 'Macintosh HD:MPW-GM:Interfaces&Libraries:Interfaces:RIncludes:' "Macintosh HD:Retro68:Retro68App" Carbon.r
|
||||
```
|
||||
|
||||
`-i` sets the include path for type declarations and `Carbon.r` tells it to use
|
||||
that file for resource descriptions. Running the command I was now rewarded
|
||||
with textual resources:
|
||||
|
||||
{{ figure(image="posts/2023/derez/MPW.png", link="posts/2023/derez/MPW.png", pixelated=true, alt="Screenshot of an MPW worksheet on Mac OS 8 showing the output of running DeRez on an application.", caption="DeRez output in MPW") }}
|
||||
|
||||
To get the text out of the VM I copied and pasted it into a new document in
|
||||
[BBEdit] (version 5.0) and saved it with Unix line endings to the Unix folder
|
||||
that [SheepShaver] shares with the host and with that I was able to update the
|
||||
[resource file in my temperature converter project][my dialog.r].
|
||||
|
||||
### Honorable Mention
|
||||
|
||||
Whilst trying to work out how to do all this I was also reminded of Ninji's
|
||||
[mpw-emu] project ([detailed write-up on their blog][mpw-emu-blog]). It
|
||||
combines an emulator with implementations of library functions in order to be
|
||||
able to run MPW tools directly (outside a Mac OS emulator). It has gained
|
||||
support for `DeRez` so you can run `DeRez` directly on a host system like
|
||||
Linux.
|
||||
|
||||
I [MacBinaried][MacBinary] `DeRez` in SheepShaver and copied it to my Linux host. Then with a bit
|
||||
of fussing with `mpw-emu` Rust code I was able to run it:
|
||||
|
||||
```
|
||||
$ mpw-emu ~/Documents/Classic\ Mac/Shared\ 2/DeRez.bin
|
||||
[2023-03-20T02:11:07Z ERROR emulator] Unimplemented call to InterfaceLib::SetFScaleDisable @10012C6C
|
||||
[2023-03-20T02:11:07Z ERROR stdio] Unimplemented format character: P
|
||||
[2023-03-20T02:11:07Z ERROR emulator] Unimplemented call to InterfaceLib::SecondsToDate @1000B2A4
|
||||
### /home/wmoore/Documents/Classic Mac/Shared 2/DeRez.bin - No filename to de-compile was specified.
|
||||
### /home/wmoore/Documents/Classic Mac/Shared 2/DeRez.bin - Usage: /home/wmoore/Documents/Classic Mac/Shared 2/DeRez.bin resourceFile [-c] [-d name[=value]] [-e] [-i path] [-m n] [-noResolve [output | include]] [-only type[(id[:id])]] [-p] [-rd] [-s type[(id[:id])]] [-script japanese | tradChinese | simpChinese | korean] [-u name] [file…].
|
||||
```
|
||||
|
||||
Amazing!
|
||||
|
||||
Unfortunately I don't think `DeRez` will work this way outside a macOS host. It
|
||||
needs to be able to read the resource fork of the application I edited with
|
||||
ResEdit and that is not preserved on Linux:
|
||||
|
||||
```
|
||||
$ mpw-emu ~/Documents/Classic\ Mac/Shared\ 2/DeRez.bin Dialog.APPL
|
||||
[2023-03-20T02:14:05Z ERROR emulator] Unimplemented call to InterfaceLib::SetFScaleDisable @10012C6C
|
||||
[2023-03-20T02:14:05Z ERROR stdio] Unimplemented format character: P
|
||||
[2023-03-20T02:14:05Z ERROR emulator] Unimplemented call to InterfaceLib::SecondsToDate @1000B2A4
|
||||
### /home/wmoore/Documents/Classic Mac/Shared 2/DeRez.bin - The resource fork of "Dialog.APPL" is empty and uninitialized.
|
||||
```
|
||||
|
||||
If you're on macOS I think that this would actually work. Although now I think
|
||||
about it Xcode ships (or at least used to) a native version of `DeRez` so now
|
||||
I'm not sure what Ninji's motivation for making it work in `mpw-emu` was.
|
||||
Perhaps it is possible to use on Linux somehow...
|
||||
|
||||
[Nim]: https://nim-lang.org/
|
||||
[ResEdit]: https://en.wikipedia.org/wiki/ResEdit
|
||||
[Retro68]: https://github.com/autc04/Retro68
|
||||
[Dialog sample]: https://github.com/autc04/Retro68/tree/5f882506013a0a8a4335350197a1b7c91763494e/Samples/Dialog
|
||||
[dialog.r]: https://github.com/autc04/Retro68/blob/5f882506013a0a8a4335350197a1b7c91763494e/Samples/Dialog/dialog.r
|
||||
[DeRez man]: https://www.manpagez.com/man/1/DeRez/
|
||||
[BBEdit]: http://www.barebones.com/products/bbedit/index.html
|
||||
[SheepShaver]: https://sheepshaver.cebix.net/
|
||||
[my dialog.r]: https://github.com/wezm/classic-mac-nim/blob/39e6ed7c2b31c20b775782319cde8ae5a43e1512/dialog.r
|
||||
[mpw-emu]: https://github.com/Treeki/mpw-emu
|
||||
[mpw-emu-blog]: https://wuffs.org/blog/emulating-mac-compilers
|
||||
[mpw]: https://en.wikipedia.org/wiki/Macintosh_Programmer%27s_Workshop
|
||||
[MacBinary]: https://en.wikipedia.org/wiki/MacBinary
|
||||
[nim-temp-src]: https://github.com/wezm/classic-mac-nim/tree/39e6ed7c2b31c20b775782319cde8ae5a43e1512
|
BIN
v2/content/posts/2023/derez/mac-nim-temp.mp4
Normal file
BIN
v2/content/posts/2023/derez/mac-nim-temp.png
Normal file
After Width: | Height: | Size: 24 KiB |
112
v2/content/posts/2023/divmod.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
+++
|
||||
title = "divmod, Rust, x86, and Optimisation"
|
||||
date = 2023-01-11T19:48:09+10:00
|
||||
|
||||
[extra]
|
||||
updated = 2023-01-11T21:11:28+10:00
|
||||
+++
|
||||
|
||||
While reviewing some Rust code that did something like this:
|
||||
|
||||
```rust
|
||||
let a = n / d;
|
||||
let b = n % d;
|
||||
```
|
||||
|
||||
I lamented the lack of a `divmod` method in Rust (that would return both the
|
||||
quotient and remainder). My colleague [Brendan] pointed out that he actually
|
||||
[added it][rust-div-mod] back in 2013 but it was moved out of the standard
|
||||
library before the 1.0 release.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
I also learned that the [`div` instruction on x86][div] provides the remainder
|
||||
so there is potentially some benefit to combining the operation. I suspected
|
||||
that LLVM was probably able to optimise the separate operations and a trip to
|
||||
[the Compiler Explorer][compiler-explorer] confirmed it.
|
||||
|
||||
This function:
|
||||
|
||||
```rust
|
||||
pub fn divmod(n: usize, d: usize) -> (usize, usize) {
|
||||
(n / d, n % d)
|
||||
}
|
||||
```
|
||||
|
||||
Compiles to the following assembly, which I have annotated with my
|
||||
understanding of each line (Note: I'm still learning x86 assembly):
|
||||
|
||||
```asm
|
||||
; rdi = numerator, rsi = denominator
|
||||
example::divmod:
|
||||
test rsi, rsi ; check for denominator of zero
|
||||
je .LBB0_5 ; jump to div zero panic if zero
|
||||
mov rax, rdi ; load rax with numerator
|
||||
or rax, rsi ; or rax with denominator
|
||||
shr rax, 32 ; shift rax right 32-bits
|
||||
je .LBB0_2 ; if the result of the shift sets the zero flag then numerator and
|
||||
; denominator are 32-bit since none of the upper 32-bits are set.
|
||||
; jump to 32-bit division implementation
|
||||
mov rax, rdi ; move numerator into rax
|
||||
xor edx, edx ; zero edx (I'm not sure why, might be relevant to the calling
|
||||
; convention and is used by the caller?)
|
||||
div rsi ; divide rax by rsi
|
||||
ret ; return, quotient is in rax, remainder in rdx
|
||||
|
||||
; 32 bit implementation
|
||||
.LBB0_2:
|
||||
mov eax, edi ; move edi to eax (32-bit regs)
|
||||
xor edx, edx ; zero edx
|
||||
div esi ; divide eax by esi
|
||||
ret
|
||||
|
||||
; div zero panic
|
||||
.LBB0_5:
|
||||
push rax
|
||||
lea rdi, [rip + str.0]
|
||||
lea rdx, [rip + .L__unnamed_1]
|
||||
mov esi, 25
|
||||
call qword ptr [rip + core::panicking::panic@GOTPCREL]
|
||||
ud2
|
||||
|
||||
.L__unnamed_2:
|
||||
.ascii "/app/example.rs"
|
||||
|
||||
.L__unnamed_1:
|
||||
.quad .L__unnamed_2
|
||||
.asciz "\017\000\000\000\000\000\000\000\002\000\000\000\006\000\000"
|
||||
|
||||
str.0:
|
||||
.ascii "attempt to divide by zero"
|
||||
```
|
||||
|
||||
I found it interesting that after checking for a zero denominator there's an
|
||||
additional check to see if the values fit into 32-bits, and if so it jumps to an
|
||||
instruction sequence that uses 32-bit registers. According to [the testing done
|
||||
in this report][timing] 32-bit `div` has lower latency—particularly on older
|
||||
models.
|
||||
|
||||
~~I wasn't able to work out why each implementation zeros `edx`. If you know,
|
||||
send me a message and I'll update the post.~~
|
||||
|
||||
**Update:** [Brion Vibber on the Fediverse][edx] provided this explanation as to why `edx`
|
||||
is being zeroed:
|
||||
|
||||
> iirc rdx / edx is the top word for the x86 division operation, which takes a double-word numerator -- the inverse of multiplication producing a double-word output.
|
||||
|
||||
This makes sense and looking back at [the docs][div] it does say that:
|
||||
|
||||
> 32-bit: Unsigned divide EDX:EAX by r/m32, with result stored in EAX := Quotient, EDX := Remainder.
|
||||
|
||||
> 64-bit: Unsigned divide RDX:RAX by r/m64, with result stored in RAX := Quotient, RDX := Remainder.
|
||||
|
||||
|
||||
|
||||
[View the Example on Compiler Explorer](https://rust.godbolt.org/z/hj9rb4Txa)
|
||||
|
||||
[Brendan]: https://github.com/brendanzab
|
||||
[rust-div-mod]: https://github.com/rust-lang/rust/commit/f39152e07baf03fc1ff4c8b2c1678ac857b4a512
|
||||
[div]: https://www.felixcloutier.com/x86/div
|
||||
[compiler-explorer]: https://rust.godbolt.org/
|
||||
[timing]: https://gmplib.org/~tege/x86-timing.pdf
|
||||
[edx]: https://bikeshed.vibber.net/@brion/109670222269686433
|
45
v2/content/posts/2023/hide-sign-in-with-google/index.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
+++
|
||||
title = "Hide Sign in With Google Pop Up"
|
||||
date = 2023-01-20T20:48:02+10:00
|
||||
|
||||
#[extra]
|
||||
#updated = 2023-01-11T21:11:28+10:00
|
||||
+++
|
||||
|
||||
Inspired by
|
||||
[Rach Smith's post on using userstyles to hide YouTube shorts][rachsmith]
|
||||
I came up with some CSS to hide those
|
||||
annoying Sign in with Google pop-ups.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
I never want to sign in with Google and use [Firefox Multi-Account Containers][containers]
|
||||
to ensure that the bulk of my browsing is done without ever being
|
||||
signed in to a Google account. This means that I see a lot of these pop ups
|
||||
encouraging me to sign in, so Google can track me more.
|
||||
|
||||
{{ figure_no(image="posts/2023/hide-sign-in-with-google/sign-in-with-google.png", border=true, width=357, alt="Screenshot of the pop up", caption="Be gone evil pop up") }}
|
||||
|
||||
I use [Stylus] to manage user styles. I created a new rule that applies to all
|
||||
sites and put this CSS in there:
|
||||
|
||||
```css
|
||||
#credential_picker_container:has(iframe[src*="accounts.google.com"]) {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
Note that it uses the [`:has`][has] selector, which is not on by default in Firefox
|
||||
at the time of writing. In Firefox 103 onwards you can enable it by toggling
|
||||
`layout.css.has-selector.enabled` (the usual caveats about poking around in
|
||||
[`about:config`][about-config] apply).
|
||||
|
||||
If you do browse while signed in to a Google account, Aranjedeath on the Fediverse
|
||||
pointed out that [you can turn them off in your account settings][Aranjedeath].
|
||||
|
||||
[containers]: https://support.mozilla.org/en-US/kb/containers
|
||||
[Aranjedeath]: https://hachyderm.io/@Aranjedeath/109720032830494381
|
||||
[rachsmith]: https://rachsmith.com/remove-youtube-shorts/
|
||||
[Stylus]: https://github.com/openstyles/stylus
|
||||
[about-config]: https://support.mozilla.org/en-US/kb/about-config-editor-firefox
|
||||
[has]: https://developer.mozilla.org/en-US/docs/Web/CSS/:has
|
After Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,44 @@
|
|||
+++
|
||||
title = "Fixing OpenBSD panic dc_atapi_start: not ready in KVM"
|
||||
date = 2023-09-17T12:13:24+10:00
|
||||
|
||||
[extra]
|
||||
updated = 2023-09-22T11:56:57+10:00
|
||||
+++
|
||||
|
||||
I tried creating an OpenBSD 7.3 virtual machine on my new computer (Arch Linux
|
||||
host) and the installer kept crashing with the error:
|
||||
|
||||
{{ figure(image="posts/2023/openbsd-db-atapi-start-not-ready/openbsd_panic_dc_atapi_start_not_ready.png", link="posts/2023/openbsd-db-atapi-start-not-ready/openbsd_panic_dc_atapi_start_not_ready.png", alt="Screenshot of the installer crash.", caption="Screenshot of the installer crash.") }}
|
||||
|
||||
```
|
||||
dc_atapi_start: not ready, st = 50
|
||||
fatal protection fault in supervisor mode trap type 4 code 0 rip ffffffff810089d9 cs 8 rflags 10282 cr2 287eb3000 cpl 6 rsp ffff800014fd11a0
|
||||
gssbase Oxffffffff818fbff0 kgsbase Ox0
|
||||
panic: trap type 4, code=0, pc=ffffffff810089d9
|
||||
syncing disks...12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 _
|
||||
```
|
||||
|
||||
<!-- more -->
|
||||
|
||||
I did a bunch of searching online and tried a several different suggestions but that one that
|
||||
worked for me was from [this Reddid thread][reddit]:
|
||||
|
||||
> **tinneriw31**
|
||||
>
|
||||
> Switch the virtual cd drive from ide to sata. Worked for me. Exact same issue.
|
||||
|
||||
I use [virt-manager] to manage VMs. These are the steps to do that when creating the VM:
|
||||
|
||||
1. Create the VM and at the last step choose "Customize configuration before install"
|
||||
2. Click on the "IDE CDROM 1" tab and change "Disk bus" to SATA
|
||||
3. Then click Apply, and then Begin installation in the top left.
|
||||
|
||||
After that the VM installed successfully.
|
||||
|
||||
{{ figure(image="posts/2023/openbsd-db-atapi-start-not-ready/virt-manager-customize-configuration.png", link="posts/2023/openbsd-db-atapi-start-not-ready/virt-manager-customize-configuration.png", alt="Screenshot of step 5 in the new virtual machine wizard in virt-manager showing the 'Customize configuration before install' option checked.", caption="Customize configuration before install.", width=504) }}
|
||||
|
||||
{{ figure(image="posts/2023/openbsd-db-atapi-start-not-ready/virt-manager-sata-cd-rom.png", link="posts/2023/openbsd-db-atapi-start-not-ready/virt-manager-sata-cd-rom.png", alt="Screenshot of the virt-manager CD ROM tab showing 'Disk bus: SATA' selected.", caption="Disk bus: SATA", width=1028) }}
|
||||
|
||||
[reddit]: https://www.reddit.com/r/openbsd/comments/12jzg2y/when_i_tried_to_install_openbsd_73_in_qemu_i/jhhk1gx/
|
||||
[virt-manager]: https://virt-manager.org/
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 153 KiB |
BIN
v2/content/posts/2023/rust-classic-mac-os-app/Ferris Weather.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 78 KiB |
297
v2/content/posts/2023/rust-classic-mac-os-app/index.md
Normal file
|
@ -0,0 +1,297 @@
|
|||
+++
|
||||
title = "Building a Classic Mac OS App in Rust"
|
||||
date = 2023-03-31T13:26:07+10:00
|
||||
|
||||
#[extra]
|
||||
#updated = 2023-03-26T14:27:05+10:00
|
||||
+++
|
||||
|
||||
Instead of using my funemployment to build useful things I have continued to
|
||||
build things for old versions of Mac OS. Through some luck and a little
|
||||
persistence I have actually managed to get Rust code running on classic Mac OS
|
||||
(I've tried Mac OS 7.5 and 8.1). In this post I'll cover how I got here and
|
||||
show a little network connected demo application I built—just in time for
|
||||
the end of [#MARCHintosh].
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Before I get into the details this is where we're headed:
|
||||
|
||||
{{ video(video="posts/2023/rust-classic-mac-os-app/ferris-weather-2023-03-31_12.37.49_edit.mp4", height=480, poster="png", preload="auto", alt="Video showing the Ferris Weather application in operation. Initially there is a window with a Ferris icon, the text, 'An application that exercises my Open Transport Rust bindings and HTTP client', and a button 'Get Weather'. Clicking the button results in an alert that says: 'The temperature in Brisbane is 26.9°C'.", caption="Ferris Weather, a weather app built with Rust (and some C)") }}
|
||||
|
||||
### DeRez Redux
|
||||
|
||||
In [the last post](@/posts/2023/derez/index.md) I got Nim code running on Mac
|
||||
OS and toyed with DeRez. [The author of `mpu-emu` replied on Mastodon][Ninji]
|
||||
letting me know that DeRez _should_ run via `mpw-emu` on Linux as the
|
||||
filesystem layer transparently handles [MacBinary] files.
|
||||
|
||||
I spent some time in the debugger and worked out that `mpw-emu` supported
|
||||
MacBinary III but [Retro68] produced MacBinary II files. I contributed code
|
||||
to `mpw-emu` to add MacBinary II support and enabled some latent support for
|
||||
UNIX paths. After that DeRez did work (almost):
|
||||
|
||||
```
|
||||
$ mpw-emu ~/Documents/Classic\ Mac/Shared\ 2/DeRez.bin Root:home:wmoore:Projects:classic-mac-rust:cmake-build-retro68ppc:Dialog.bin
|
||||
[2023-03-31T04:51:34Z ERROR emulator] Unimplemented call to InterfaceLib::SetFScaleDisable @10012C6C
|
||||
[2023-03-31T04:51:34Z ERROR stdio] Unimplemented format character: P
|
||||
[2023-03-31T04:51:34Z ERROR emulator] Unimplemented call to InterfaceLib::SecondsToDate @1000B2A4
|
||||
data 'DITL' (128) {
|
||||
$"0007 0000 0000 00A0 00E6 00B4 0136 0404" /* .......†.Ê.¥.6.. */
|
||||
$"5175 6974 0000 0000 009B 00E1 00B9 013B" /* Quit.....õ.·.π.; */
|
||||
$"0000 0000 0000 0046 000A 005A 0136 0818" /* .......F...Z.6.. */
|
||||
$"436F 6E76 6572 7369 6F6E 2070 6F77 6572" /* Conversion power */
|
||||
$"6564 2062 7920 5E30 0000 0000 001E 000A" /* ed by ^0........ */
|
||||
$"003E 002A A002 0080 0000 0000 0014 0032" /* .>.*†..Ä.......2 */
|
||||
$"0024 007D 8807 4365 6C73 6975 7300 0000" /* .$.}à.Celsius... */
|
||||
$"0000 0014 00AA 0024 00F5 8809 4661 7265" /* .....™.$.ıà∆Fare */
|
||||
$"6E68 6569 7400 0000 0000 0029 0036 0039" /* nheit......).6.9 */
|
||||
$"0081 1002 3235 0000 0000 002B 00AE 003B" /* .Å..25.....+.Æ.; */
|
||||
$"00F9 1002 3737" /* .˘..77 */
|
||||
};
|
||||
|
||||
/* etc */
|
||||
```
|
||||
|
||||
Success! DeRez running on Linux… only thing is that when you point at the type
|
||||
definitions to get structured output instead of hex dumps it hits an
|
||||
unimplemented function in `mpw-emu`. It's on my to-do list to fix that:
|
||||
|
||||
```
|
||||
$ mpw-emu ~/Documents/Classic\ Mac/Shared\ 2/DeRez.bin Root:home:wmoore:Projects:classic-mac-rust:cmake-build-retro68ppc:Dialog.bin Root:home:wmoore:Source:github.com:autc04:Retro68:InterfacesAndLibraries:Interfaces\&Libraries:Interfaces:RIncludes:Carbon.r
|
||||
[2023-03-31T04:53:07Z ERROR emulator] Unimplemented call to InterfaceLib::SetFScaleDisable @10012C6C
|
||||
[2023-03-31T04:53:07Z ERROR stdio] Unimplemented format character: P
|
||||
[2023-03-31T04:53:07Z ERROR emulator] Unimplemented call to InterfaceLib::SecondsToDate @1000B2A4
|
||||
[2023-03-31T04:53:07Z ERROR emulator] Unimplemented call to StdCLib::fseek @10006A8C
|
||||
File "Root:home:wmoore:Source:github.com:autc04:Retro68:InterfacesAndLibraries:Interfaces&Libraries:Interfaces:RIncludes:CoreServices.r"; Line 0; ### /home/wmoore/Documents/Classic Mac/Shared 2/DeRez.bin - Can't FSeek on file Root:home:wmoore:Source:github.com:autc04:Retro68:InterfacesAndLibraries:Interfaces&Libraries:Interfaces:RIncludes:CoreServices.r.
|
||||
File "Root:home:wmoore:Source:github.com:autc04:Retro68:InterfacesAndLibraries:Interfaces&Libraries:Interfaces:RIncludes:CoreServices.r"; Line 0; ### /home/wmoore/Documents/Classic Mac/Shared 2/DeRez.bin - Fatal Error, can't recover.
|
||||
```
|
||||
|
||||
### MacBinary
|
||||
|
||||
Poking at the MacBinary code in `mpw-emu` got me wondering if there was already
|
||||
a MacBinary crate that could be used. Turns out there wasn't so I somehow
|
||||
nerd-sniped myself into [building one][macbinary-crate].
|
||||
|
||||
The first challenge was finding a decent specification for the three versions
|
||||
of MacBinary. I was eventually I was able to dig up the following:
|
||||
|
||||
- [MacBinary I]
|
||||
- [MacBinary II]
|
||||
- [MacBinary III]
|
||||
|
||||
I then set about building the parser. I reused the binary parser code from
|
||||
[Allsorts] since I was already familiar with that code. I hit another roadblock
|
||||
when it came to the CRC in the header. Nothing describes the actual CRC
|
||||
algorithm used. I tried the CRC reversing tool [CRC RevEng][RevEng] without
|
||||
success. A lot of existing code seemed to use an implementation that originated
|
||||
in a late 80's UNIX utility, [mcvert], that has unclear licensing. I wanted to
|
||||
use the Rust [crc crate] instead.
|
||||
|
||||
I eventually stumbled on the blog post,
|
||||
[Detecting MacBinary format](https://entropymine.wordpress.com/2019/02/13/detecting-macbinary-format/),
|
||||
which included the line:
|
||||
|
||||
> Note that the spec does not even tell you what CRC algorithm to use — you
|
||||
> have to be a detective to figure it out. (It’s the one sometimes called
|
||||
> CRC16-CCITT.)
|
||||
|
||||
That was the tip I needed and with a little trial an error I eventually worked
|
||||
out that it was [CRC-16/XMODEM] also known as `CRC-16/CCITT-FALSE`. In
|
||||
hindsight I could probably have worked this out from the discussion of XMODEM
|
||||
in the [MacBinary I spec][MacBinary I].
|
||||
|
||||
With that sorted I was able to wrap up the parser and do some testing. I could
|
||||
now read the resource and data forks and figured it would be interesting to be
|
||||
able to parse the resource data too, so I added a resource fork parser as well.
|
||||
|
||||
I wrote the parsers in a way that does not require heap allocation—only
|
||||
borrowing from the underlying data. Due to this it was straightforward to make
|
||||
the crate compatible with `no_std`, which allows it to be used in embedded
|
||||
environments and WebAssembly.
|
||||
|
||||
As something of a test-bed I created some
|
||||
WebAssembly bindings and built a page that allows you inspect MacBinary
|
||||
files online, with all parsing done client-side via the crate. You can
|
||||
try it out at: <https://7bit.org/macbinary/>
|
||||
|
||||
### Rust on Mac OS
|
||||
|
||||
Now that I was well and truly in the classic Mac space again I took another
|
||||
stab at compiling Rust for PPC Mac OS
|
||||
([see this post for my previous attempt](@/posts/2023/rust-on-ppc-classic-mac-os/index.md)).
|
||||
It seemed that using the
|
||||
`powerpc-ibm-aix` LLVM target was most likely to produce a compatible library
|
||||
(Apple used AIX conventions for PPC Mac OS). Problem was that it was hitting
|
||||
unimplemented code in LLVM:
|
||||
|
||||
> LLVM ERROR: relocation for paired relocatable term is not yet supported
|
||||
|
||||
I set about trying to work out how this code path was being hit and ran `rustc`
|
||||
in a debugger. Unsurprisingly there were no debug symbols so I built `rustc`
|
||||
and LLVM from source. This was my `config.toml` for the Rust repo:
|
||||
|
||||
```toml
|
||||
[llvm]
|
||||
release-debuginfo = true
|
||||
download-ci-llvm = false
|
||||
link-jobs = 4
|
||||
```
|
||||
|
||||
After repeatedly running out of disk space and memory compiling LLVM (the
|
||||
binaries with debug info are huge) I eventually had new Rust compiler.
|
||||
|
||||
Some of the LLVM binaries:
|
||||
|
||||
```
|
||||
.rwxr-xr-x 2.0G wmoore 26 Mar 20:05 llc
|
||||
.rwxr-xr-x 2.1G wmoore 26 Mar 20:10 llvm-opt-fuzzer
|
||||
.rwxr-xr-x 2.1G wmoore 26 Mar 20:04 bugpoint
|
||||
.rwxr-xr-x 2.2G wmoore 26 Mar 20:09 llvm-lto2
|
||||
.rwxr-xr-x 2.2G wmoore 26 Mar 20:06 llvm-lto
|
||||
.rwxr-xr-x 2.2G wmoore 26 Mar 20:11 opt
|
||||
.rwxr-xr-x 2.3G wmoore 26 Mar 20:11 llvm-reduce
|
||||
```
|
||||
|
||||
I linked the new compiler into `rustup` and then repeated my previous steps in
|
||||
the debugger… except this time the code compiled and did not hit the
|
||||
unimplemented LLVM code. This was my first lucky break. I'm not sure what
|
||||
changed but it was now happily compiling the code. I switched to a recent
|
||||
nightly compiler and that worked too! No need to build from source.
|
||||
|
||||
I repeated the step described in my original post of using
|
||||
`powerpc-linux-gnu-objcopy` to convert the static library archive (`.a`) to a
|
||||
format that Retro68 would accept. After some fighting with `binutils` I was
|
||||
finally able to get it to link!
|
||||
|
||||
<iframe src="https://mastodon.decentralised.social/@wezm/110098546361010915/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"></iframe><script src="https://mastodon.decentralised.social/embed.js" async="async"></script>
|
||||
|
||||
I rebuilt the temperature converter that I'd built in Nim in Rust ([source code][rust-temp]) and ran into
|
||||
more linker/`binutils` issues. After a _lot_ of trial-and-error and some more
|
||||
luck I was able to solve that by using the updated `binutils` on the
|
||||
`gcc12-update branch` branch of Retro68. I now had a working temperature
|
||||
converter:
|
||||
|
||||
{{ video(video="posts/2023/rust-classic-mac-os-app/classic-mac-rust-2023-03-28_20.00.31.mp4", height=480, poster="png", preload="auto", alt="Video of the temperature converter converting values to and from Celsius, running on Mac OS 8.1 (in emulator).", caption="The temperature converter application ported to Rust") }}
|
||||
|
||||
It worked on Mac OS 7.5 too:
|
||||
|
||||
{{ figure(image="posts/2023/rust-classic-mac-os-app/rust-on-mac-os-7.png", link="posts/2023/rust-classic-mac-os-app/rust-on-mac-os-7.png", pixelated=true, border=1, alt="Screenshot of the temperature converter application running on Mac OS 7.5 (in emulator).", caption="Temperature converter application running on Mac OS 7.5") }}
|
||||
|
||||
The Rust version is a bit more efficient than the [Nim version][nim-version] as
|
||||
it avoids some copying and heap allocation. That latter of which because I'm
|
||||
coding in a `no_std` environment without a heap.
|
||||
|
||||
The Rust standard library is divided into three main parts (crates):
|
||||
|
||||
1. `core` for things that do not require heap allocation, I/O, etc.
|
||||
2. `alloc` for things that use heap allocation but not I/O etc.
|
||||
3. `std`, the rest: files, networking, threads, etc. `std` re-exports the
|
||||
other two.
|
||||
|
||||
By defining a custom allocator that called `malloc` and `free` provided by the
|
||||
Retro68 environment I was able to use the `alloc` crate in addition to `core`.
|
||||
This gained me access `String`, `Vec`, and friends.
|
||||
|
||||
#### Networking
|
||||
|
||||
I now wanted to build something a little more involved than a single dialog. I
|
||||
set about building bindings to Open Transport, Apple's network stack introduced
|
||||
with PCI Power Macs (like my 9500).
|
||||
|
||||
Due to its heritage most of the Mac OS toolbox functions use the Pascal calling
|
||||
convention, [which LLVM does not support][llvm-pascal]. To bridge the C (and
|
||||
Rust) world to this Pascal world I had to create [trampoline functions] in C
|
||||
for each toolbox function that I wanted to call from Rust (if there's a better
|
||||
way to do this I'd love to know how). This works because `gcc` in Retro68
|
||||
understands both C and Pascal calling conventions. I appended an underscore to
|
||||
each of the wrapper functions. For example:
|
||||
|
||||
```c
|
||||
OSStatus OTConnect_(EndpointRef ref, TCall *sndCall, TCall *rcvCall) {
|
||||
return OTConnect(ref, sndCall, rcvCall);
|
||||
}
|
||||
```
|
||||
|
||||
I used the "Downloading a URL With HTTP" example from the [Networking With Open Transport]
|
||||
book as a guide for the functions I needed. Once the bindings were created I
|
||||
implemented the `TcpClientStack` trait from the [embedded-nal] \(embedded network abstraction layer)
|
||||
crate against Open Transport. Next I used this with the [http_io] crate to be able
|
||||
to make HTTP requests.
|
||||
|
||||
As an initial test I wrote an app to fetch a friend's website (since it's
|
||||
available over plain HTTP) and show an alert with the number of bytes read.
|
||||
Amazingly this worked on the first try: the Open Transport bindings, the
|
||||
`TcpClientStack` implementation, the HTTP client, and my test code all worked!
|
||||
|
||||
Finally I used my newfound networking abilities to build [Ferris
|
||||
Weather][ferris-weather], the application shown at the start of the post. This
|
||||
uses the HTTP client to fetch a JSON file containing weather observations,
|
||||
parses it with [serde] and then shows an alert with the most recent
|
||||
observation. I also drew a little 1-bit [Ferris the Rustacean][Ferris] in
|
||||
[ResEdit] for it.
|
||||
|
||||
The idea for this was prompted by the [Australian Bureau of Meteorology][bom]
|
||||
still being accessible over HTTP. Unfortunately it wasn't working and after a
|
||||
lot of debugging I eventually discovered that I triggering their anti-scraping
|
||||
blocker for some reason. To work around this I copied a snapshot of the JSON to
|
||||
my own server. So, unfortunately the data shown by the application does not
|
||||
update but you still get the idea.
|
||||
|
||||
{{ figure(image="posts/2023/rust-classic-mac-os-app/Ferris%20Weather.png", link="posts/2023/rust-classic-mac-os-app/Ferris%20Weather.png", pixelated=true, border=1, alt="Screenshot of the Ferris Weather application showing an alert with the temperature in Brisbane.", caption="Ferris Weather") }}
|
||||
|
||||
So there you have it, that's how I built an application in Rust (and some C)
|
||||
for classic Mac OS. The [source code to Ferris Weather][ferris-weather] is on
|
||||
GitHub.
|
||||
|
||||
### Next
|
||||
|
||||
My intention is to take a bit of a break from classic Mac OS for a bit and work
|
||||
on some other projects—ones that might be useful to people in this century—but
|
||||
there are some things I want to look at when I come back to it:
|
||||
|
||||
First is TLS support for the HTTP client. I think this should be relatively
|
||||
straightforward with the [embedded-tls] crate.
|
||||
|
||||
Next I'd like to improve how Open Transport is used. I think with either
|
||||
the synchronous, non-blocking mode I'm using now or the asynchronous mode it
|
||||
should be possible to tie it into the async Rust ecosystem, which would allow
|
||||
it to play nicer with the event loop and cooperative multi-tasking.
|
||||
|
||||
Finally, so far I've been working without the full Rust standard library, only
|
||||
`core` and `alloc`. It seems like it should be possible to implement a lot of
|
||||
the remaining standard library (io, networking), on top of the Mac OS toolbox,
|
||||
but that's a lot of work and will have to wait for another time.
|
||||
|
||||
### Hire Me
|
||||
|
||||
As mentioned at the start of this post I'm currently taking a break from
|
||||
employment but I will be looking for a new role next month, so if you're looking
|
||||
for a Rust developer get in touch.
|
||||
|
||||
[#MARCHintosh]: https://www.marchintosh.com/
|
||||
[Ninji]: https://vulpine.club/@Ninji/110053455721324087
|
||||
[mcvert]: https://web.mit.edu/~mkgray/jik/sipbsrc/src/mcvert/mcvert.c
|
||||
[Networking With Open Transport]: https://developer.apple.com/library/archive/documentation/mac/NetworkingOT/NetworkingOpenTransport.pdf
|
||||
[ferris-weather]: https://github.com/wezm/ferris-weather
|
||||
[Retro68]: https://github.com/autc04/Retro68
|
||||
[MacBinary]: https://en.wikipedia.org/wiki/MacBinary
|
||||
[macbinary-crate]: https://lib.rs/crates/macbinary
|
||||
[Allsorts]: https://github.com/yeslogic/allsorts
|
||||
[RevEng]: https://reveng.sourceforge.io/
|
||||
[crc crate]: https://lib.rs/crates/crc
|
||||
[CRC-16/XMODEM]: https://reveng.sourceforge.io/crc-catalogue/16.htm#crc.cat.crc-16-ibm-3740
|
||||
[MacBinary I]: https://web.archive.org/web/20050307030202/http://www.lazerware.com/formats/macbinary/macbinary.html
|
||||
[MacBinary II]: https://web.archive.org/web/20050305042909/http://www.lazerware.com/formats/macbinary/macbinary_ii.html
|
||||
[MacBinary III]: https://web.archive.org/web/20050305044255/http://www.lazerware.com/formats/macbinary/macbinary_iii.html
|
||||
[llvm-pascal]: https://github.com/llvm/llvm-project/blob/bd20a344bbf813b2c39b57ad1a5248bff915ce25/clang/lib/CodeGen/CGCall.cpp#L60
|
||||
[trampoline functions]: https://en.wikipedia.org/wiki/Trampoline_(computing)
|
||||
[embedded-nal]: https://docs.rs/embedded-nal/latest/embedded_nal/
|
||||
[http_io]: https://lib.rs/crates/http_io
|
||||
[serde]: https://serde.rs/
|
||||
[bom]: http://www.bom.gov.au/
|
||||
[Ferris]: https://rustacean.net/
|
||||
[ResEdit]: https://en.wikipedia.org/wiki/ResEdit
|
||||
[embedded-tls]: https://lib.rs/crates/embedded-tls
|
||||
[rust-temp]: https://github.com/wezm/classic-mac-rust
|
||||
[nim-version]: https://github.com/wezm/classic-mac-nim
|
After Width: | Height: | Size: 6.7 KiB |
BIN
v2/content/posts/2023/rust-on-ppc-classic-mac-os/ParamText.jpg
Normal file
After Width: | Height: | Size: 609 KiB |
After Width: | Height: | Size: 62 KiB |
451
v2/content/posts/2023/rust-on-ppc-classic-mac-os/index.md
Normal file
|
@ -0,0 +1,451 @@
|
|||
+++
|
||||
title = "Trying to Run Rust on Classic Mac OS"
|
||||
date = 2023-02-27T10:06:28+10:00
|
||||
|
||||
[extra]
|
||||
updated = 2023-03-26T14:27:05+10:00
|
||||
+++
|
||||
|
||||
I recently acquired a Power Macintosh 9500/150 and after cleaning it up and
|
||||
building a [BlueSCSI] to replace the failed hard drive it's now in a
|
||||
semi-operational state. This weekend I thought I'd see if I could build a Mac
|
||||
app for it that called some Rust code. This post details my trials and
|
||||
tribulations.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
I started by building [Retro68], which is a modernish GCC based toolchain
|
||||
that allows cross-compiling applications for 68K and PPC Macs. With Retro68
|
||||
built I set up a VM in [SheepShaver] running Mac OS 8.1. Using the LaunchAAPL
|
||||
and LaunchAAPLServer tools that come with Retro68 I was able to build the
|
||||
sample applications and launch them in the emulated Mac.
|
||||
|
||||
With the basic workflow working I set about creating a Rust project that built
|
||||
a static library with one very basic exported function. It just returns a
|
||||
static [Pascal string] when called.
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![feature(lang_items)]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
static MSG: &[u8] = b"\x04Rust";
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hello_rust() -> *const u8 {
|
||||
MSG.as_ptr()
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_panic: &PanicInfo<'_>) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[lang = "eh_personality"]
|
||||
extern "C" fn eh_personality() {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_msg_is_pascal_string() {
|
||||
assert_eq!(MSG[0], MSG[1..].len().try_into().unwrap());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Classic Mac OS is not a target that Rust knows about so I created a custom
|
||||
target JSON definition named `powerpc-apple-macos.json` based on prior work by
|
||||
kmeisthax in [this GitHub discussion](https://github.com/autc04/Retro68/discussions/123#discussioncomment-597268):
|
||||
|
||||
```json
|
||||
{
|
||||
"arch": "powerpc",
|
||||
"data-layout": "E-m:a-p:32:32-i64:64-n32",
|
||||
"executables": true,
|
||||
"llvm-target": "powerpc-unknown-none",
|
||||
"max-atomic-width": 32,
|
||||
"os": "macosclassic",
|
||||
"vendor": "apple",
|
||||
"target-endian": "big",
|
||||
"target-pointer-width": "32",
|
||||
"linker": "powerpc-apple-macos-gcc",
|
||||
"linker-flavor": "gcc",
|
||||
"linker-is-gnu": true
|
||||
}
|
||||
```
|
||||
|
||||
I was able to build the static library with this cargo invocation:
|
||||
|
||||
```
|
||||
cargo +nightly build --release -Z build-std=core --target powerpc-apple-macos.json
|
||||
```
|
||||
|
||||
It's using nightly because it's using unstable features to build `core` and the
|
||||
`eh_personality` lang item in the code.
|
||||
|
||||
This successfully compiles and produces
|
||||
`target/powerpc-apple-macos/release/libclassic_mac_rust.a`
|
||||
|
||||
I used the [Dialog sample] from Retro68 as the basis of my Mac app. Here it is
|
||||
running prior to Rust integration:
|
||||
|
||||
{{ figure(image="posts/2023/rust-on-ppc-classic-mac-os/dialog-sample.png", link="posts/2023/rust-on-ppc-classic-mac-os/dialog-sample.png", alt="Screenshot of SheepShaver running Mac OS 8.1. It shows some Finder windows with a frontmost dialog that has a text label, text field, radio buttons, check box and Quit button.", caption="Dialog Sample") }}
|
||||
|
||||
This is my tweaked version of the C file:
|
||||
|
||||
```c
|
||||
/*
|
||||
Copyright 2015 Wolfgang Thaller.
|
||||
|
||||
This file is part of Retro68.
|
||||
|
||||
Retro68 is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Retro68 is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Retro68. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Quickdraw.h>
|
||||
#include <Dialogs.h>
|
||||
#include <Fonts.h>
|
||||
|
||||
#ifndef TARGET_API_MAC_CARBON
|
||||
/* NOTE: this is checking whether the Dialogs.h we use *knows* about Carbon,
|
||||
not whether we are actually compiling for Cabon.
|
||||
If Dialogs.h is older, we add a define to be able to use the new name
|
||||
for NewUserItemUPP, which used to be NewUserItemProc. */
|
||||
|
||||
#define NewUserItemUPP NewUserItemProc
|
||||
#endif
|
||||
|
||||
extern ConstStringPtr hello_rust(void);
|
||||
|
||||
pascal void ButtonFrameProc(DialogRef dlg, DialogItemIndex itemNo)
|
||||
{
|
||||
DialogItemType type;
|
||||
Handle itemH;
|
||||
Rect box;
|
||||
|
||||
GetDialogItem(dlg, 1, &type, &itemH, &box);
|
||||
InsetRect(&box, -4, -4);
|
||||
PenSize(3,3);
|
||||
FrameRoundRect(&box,16,16);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
#if !TARGET_API_MAC_CARBON
|
||||
InitGraf(&qd.thePort);
|
||||
InitFonts();
|
||||
InitWindows();
|
||||
InitMenus();
|
||||
TEInit();
|
||||
InitDialogs(NULL);
|
||||
#endif
|
||||
DialogPtr dlg = GetNewDialog(128,0,(WindowPtr)-1);
|
||||
InitCursor();
|
||||
SelectDialogItemText(dlg,4,0,32767);
|
||||
|
||||
ConstStr255Param param1 = hello_rust();
|
||||
|
||||
ParamText(param1, "\p", "\p", "\p");
|
||||
|
||||
DialogItemType type;
|
||||
Handle itemH;
|
||||
Rect box;
|
||||
|
||||
GetDialogItem(dlg, 2, &type, &itemH, &box);
|
||||
SetDialogItem(dlg, 2, type, (Handle) NewUserItemUPP(&ButtonFrameProc), &box);
|
||||
|
||||
ControlHandle cb, radio1, radio2;
|
||||
GetDialogItem(dlg, 5, &type, &itemH, &box);
|
||||
cb = (ControlHandle)itemH;
|
||||
GetDialogItem(dlg, 6, &type, &itemH, &box);
|
||||
radio1 = (ControlHandle)itemH;
|
||||
GetDialogItem(dlg, 7, &type, &itemH, &box);
|
||||
radio2 = (ControlHandle)itemH;
|
||||
SetControlValue(radio1, 1);
|
||||
|
||||
short item;
|
||||
do {
|
||||
ModalDialog(NULL, &item);
|
||||
|
||||
if(item >= 5 && item <= 7)
|
||||
{
|
||||
if(item == 5)
|
||||
SetControlValue(cb, !GetControlValue(cb));
|
||||
if(item == 6 || item == 7)
|
||||
{
|
||||
SetControlValue(radio1, item == 6);
|
||||
SetControlValue(radio2, item == 7);
|
||||
}
|
||||
}
|
||||
} while(item != 1);
|
||||
|
||||
FlushEvents(everyEvent, -1);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
And this is the resource file (`dialog.r`):
|
||||
|
||||
```
|
||||
/*
|
||||
Copyright 2015 Wolfgang Thaller.
|
||||
|
||||
This file is part of Retro68.
|
||||
|
||||
Retro68 is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Retro68 is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Retro68. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Dialogs.r"
|
||||
|
||||
resource 'DLOG' (128) {
|
||||
{ 50, 100, 240, 420 },
|
||||
dBoxProc,
|
||||
visible,
|
||||
noGoAway,
|
||||
0,
|
||||
128,
|
||||
"",
|
||||
centerMainScreen
|
||||
};
|
||||
|
||||
resource 'DITL' (128) {
|
||||
{
|
||||
{ 190-10-20, 320-10-80, 190-10, 320-10 },
|
||||
Button { enabled, "Quit" };
|
||||
|
||||
{ 190-10-20-5, 320-10-80-5, 190-10+5, 320-10+5 },
|
||||
UserItem { enabled };
|
||||
|
||||
{ 10, 10, 30, 310 },
|
||||
StaticText { enabled, "Hello ^0" };
|
||||
|
||||
{ 40, 10, 56, 310 },
|
||||
EditText { enabled, "Edit Text Item" };
|
||||
|
||||
{ 70, 10, 86, 310 },
|
||||
CheckBox { enabled, "Check Box" };
|
||||
|
||||
{ 90, 10, 106, 310 },
|
||||
RadioButton { enabled, "Radio 1" };
|
||||
|
||||
{ 110, 10, 126, 310 },
|
||||
RadioButton { enabled, "Radio 2" };
|
||||
}
|
||||
};
|
||||
|
||||
#include "Processes.r"
|
||||
|
||||
resource 'SIZE' (-1) {
|
||||
reserved,
|
||||
acceptSuspendResumeEvents,
|
||||
reserved,
|
||||
canBackground,
|
||||
doesActivateOnFGSwitch,
|
||||
backgroundAndForeground,
|
||||
dontGetFrontClicks,
|
||||
ignoreChildDiedEvents,
|
||||
is32BitCompatible,
|
||||
#ifdef TARGET_API_MAC_CARBON
|
||||
isHighLevelEventAware,
|
||||
#else
|
||||
notHighLevelEventAware,
|
||||
#endif
|
||||
onlyLocalHLEvents,
|
||||
notStationeryAware,
|
||||
dontUseTextEditServices,
|
||||
reserved,
|
||||
reserved,
|
||||
reserved,
|
||||
#ifdef TARGET_API_MAC_CARBON
|
||||
500 * 1024, // Carbon apparently needs additional memory.
|
||||
500 * 1024
|
||||
#else
|
||||
100 * 1024,
|
||||
100 * 1024
|
||||
#endif
|
||||
};
|
||||
```
|
||||
|
||||
The main differences are:
|
||||
|
||||
* `extern` declaration for the Rust function
|
||||
* Using the string returned from `hello_rust` to set `ParamText`
|
||||
* Changing the StaticText control's text to "Hello ^0" in order to make use of
|
||||
the `ParamText`
|
||||
* Adding `target_link_libraries(Dialog ${CMAKE_SOURCE_DIR}/target/powerpc-apple-macos/release/libclassic_mac_rust.a)`
|
||||
to `CMakeLists.txt` to have CMake link with the Rust library.
|
||||
|
||||
{{ figure(image="posts/2023/rust-on-ppc-classic-mac-os/ParamText.jpg", link="posts/2023/rust-on-ppc-classic-mac-os/ParamText.jpg", alt="Photo of ParamText documentation from my copy of Inside Macintosh Volume Ⅰ", caption="ParamText documentation from my copy of Inside Macintosh Volume Ⅰ") }}
|
||||
|
||||
Now when building the project we get…
|
||||
|
||||
```
|
||||
ninja: Entering directory `cmake-build-retro68ppc'
|
||||
[1/4] Linking C executable Dialog.xcoff
|
||||
FAILED: Dialog.xcoff
|
||||
: && /home/wmoore/Source/github.com/autc04/Retro68-build/toolchain/bin/powerpc-apple-macos-gcc -Wl,-gc-sections CMakeFiles/Dialog.dir/dialog.obj -o Dialog.xcoff /home/wmoore/Projects/classic-mac-rust/target/powerpc-apple-macos/release/libclassic_mac_rust.a && :
|
||||
/home/wmoore/Source/github.com/autc04/Retro68-build/toolchain/lib/gcc/powerpc-apple-macos/9.1.0/../../../../powerpc-apple-macos/bin/ld:/home/wmoore/Projects/classic-mac-rust/target/powerpc-apple-macos/release/libclassic_mac_rust.a: file format not recognized; treating as linker script
|
||||
/home/wmoore/Source/github.com/autc04/Retro68-build/toolchain/lib/gcc/powerpc-apple-macos/9.1.0/../../../../powerpc-apple-macos/bin/ld:/home/wmoore/Projects/classic-mac-rust/target/powerpc-apple-macos/release/libclassic_mac_rust.a:1: syntax error
|
||||
collect2: error: ld returned 1 exit status
|
||||
ninja: build stopped: subcommand failed.
|
||||
```
|
||||
|
||||
It doesn't like `libclassic_mac_rust.a`. Some investigation shows that the objects in the library
|
||||
are in ELF format. `powerpc-apple-macos-objcopy --info` shows that Retro68 does not handle
|
||||
ELF:
|
||||
|
||||
```
|
||||
BFD header file version (GNU Binutils) 2.31.1
|
||||
xcoff-powermac
|
||||
(header big endian, data big endian)
|
||||
powerpc:common
|
||||
rs6000:6000
|
||||
srec
|
||||
(header endianness unknown, data endianness unknown)
|
||||
powerpc:common
|
||||
rs6000:6000
|
||||
symbolsrec
|
||||
(header endianness unknown, data endianness unknown)
|
||||
powerpc:common
|
||||
rs6000:6000
|
||||
verilog
|
||||
(header endianness unknown, data endianness unknown)
|
||||
powerpc:common
|
||||
rs6000:6000
|
||||
tekhex
|
||||
(header endianness unknown, data endianness unknown)
|
||||
powerpc:common
|
||||
rs6000:6000
|
||||
binary
|
||||
(header endianness unknown, data endianness unknown)
|
||||
powerpc:common
|
||||
rs6000:6000
|
||||
ihex
|
||||
(header endianness unknown, data endianness unknown)
|
||||
powerpc:common
|
||||
rs6000:6000
|
||||
|
||||
xcoff-powermac srec symbolsrec verilog tekhex binary ihex
|
||||
powerpc:common xcoff-powermac srec symbolsrec verilog tekhex binary ihex
|
||||
rs6000:6000 xcoff-powermac srec symbolsrec verilog tekhex binary ihex
|
||||
```
|
||||
|
||||
It looks like it really only supports `xcoff-powermac`, which was derived from
|
||||
rs6000 AIX. At this point I tried to find a way to convert my ELF objects to
|
||||
XCOFF. I eventually stumbled across
|
||||
[this thread on the Haiku forum](https://discuss.haiku-os.org/t/xcoff-pef/12445/15)
|
||||
that mentions that `powerpc-linux-gnu-binutils` on Debian knows about
|
||||
`aixcoff-rs6000`. So I fired up a Debian docker container and tried converting
|
||||
my `.a`, and it worked:
|
||||
|
||||
```
|
||||
docker run --rm -it -v $(pwd):/src debian:testing
|
||||
apt update
|
||||
apt install binutils-powerpc-linux-gnu
|
||||
powerpc-linux-gnu-objcopy -O aixcoff-rs6000 /src/target/powerpc-apple-macos/release/libclassic_mac_rust.a /src/target/powerpc-apple-macos/release/libclassic_mac_rust.obj
|
||||
```
|
||||
|
||||
Examining the objects in the new archive showed that they were now in the same
|
||||
format as the objects generated by Retro68. I updated the `CMakeLists.txt` to
|
||||
point at the new library and tried building again:
|
||||
|
||||
```
|
||||
/home/wmoore/Source/github.com/autc04/Retro68-build/toolchain/lib/gcc/powerpc-apple-macos/9.1.0/../../../../powerpc-apple-macos/bin/ld: /home/wmoore/Projects/classic-mac-rust/target/powerpc-apple-macos/release/libclassic_mac_rust.obj(classic_mac_rust-80e61781bab75910.classic_mac_rust.9ba2ce33-cgu.0.rcgu.o): class 2 symbol `hello_rust' has no aux entries
|
||||
```
|
||||
|
||||
Now we get further. It can read the `.a` now and even sees the `hello_rust`
|
||||
symbol but it
|
||||
[looks like it's looking for an aux entry to determine the symbol type](https://github.com/autc04/Retro68/blob/5f882506013a0a8a4335350197a1b7c91763494e/binutils/bfd/xcofflink.c#L1461-L1478)
|
||||
but not finding one. AUX entries are an
|
||||
[XCOFF](https://www.ibm.com/docs/en/aix/7.2?topic=formats-xcoff-object-file-format)
|
||||
thing.
|
||||
|
||||
One other thing I tried was setting the `llvm-target` in the custom target JSON
|
||||
to `powerpc-ibm-aix`. Due to the heritage of PPC Mac OS the ABI is the same
|
||||
(Apple used the AIX toolchain, which is why object files use XCOFF even though
|
||||
executables use PEF). This target would be ideal as it would use the right ABI
|
||||
and emit XCOFF by default.
|
||||
|
||||
Unfortunately it runs into unimplemented parts of LLVM's XCOFF implementation:
|
||||
|
||||
> LLVM ERROR: relocation for paired relocatable term is not yet supported
|
||||
|
||||
Rust uses a fork/snapshot of LLVM but the
|
||||
[issue is still present in LLVM master](https://github.com/rust-lang/llvm-project/blob/5ef9f9948fca7cb39dd6c1935ca4e819fb7a0db2/llvm/lib/MC/XCOFFObjectWriter.cpp).
|
||||
[This post on writing a Mac OS 9 application in Swift][swift] goes down a
|
||||
similar path using the AIX target and also mentions patching the Swift compiler
|
||||
to avoid the unsupported parts of LLVMs XCOFF implementation. That's an avenue
|
||||
for future experimentation.
|
||||
|
||||
### rustc\_codegen\_gcc
|
||||
|
||||
At this point I decided to try a different approach.
|
||||
[rustc\_codegen\_gcc](https://github.com/rust-lang/rustc_codegen_gcc) is a
|
||||
codegen plugin that uses [libgccjit] for code generation instead of LLVM. The
|
||||
motivation of the project is promising for my use case:
|
||||
|
||||
> The primary goal of this project is to be able to compile Rust code on
|
||||
> platforms unsupported by LLVM.
|
||||
|
||||
I found the instructions for using `rustc_codegen_gcc` a bit difficult to
|
||||
follow, especially when trying to build a cross-compiler.
|
||||
|
||||
I eventually managed to rebuild Retro68 with `libgccjit` enabled and then coax
|
||||
`rustc_codegen_gcc` to use it. Unsurprisingly that quickly failed as Retro68 is
|
||||
based on GCC 9.1 and `rustc_codegen_gcc` is building against GCC master and
|
||||
there were many missing symbols.
|
||||
|
||||
Undeterred I noted that there is a WIP GCC 12.2 branch in the Retro68 repo so I
|
||||
built that and tweaked `rustc_codegen_gcc` to disable the `master` cargo
|
||||
feature that should in theory allow it to build against a GCC release. This did
|
||||
in fact allow me to get a bit further but I ran into more issues in the step
|
||||
that attempts to build `compiler-rt` and `core`. Eventually I gave up on this
|
||||
route too. I was probably too far off the well tested configuration of x86,
|
||||
against GCC master.
|
||||
|
||||
Future work here is to trying building a `powerpc-ibm-aix` libgccjit from GCC
|
||||
master and see if that works.
|
||||
|
||||
### Wrap Up
|
||||
|
||||
[Bastian on Twitter](https://twitter.com/turbolent/status/1617231570573873152)
|
||||
has had some success compiling Rust to Web Assembly, Web Assembly to C89, C89
|
||||
to Mac OS 9 binary, which is definitely cool but I would still love to be able
|
||||
to generate native PPC code directly from `rustc` somehow.
|
||||
|
||||
This is where I have parked this project for now. I actually only discovered
|
||||
the post on building a Mac OS 9 application with Swift while writing this post.
|
||||
There are perhaps some ideas in there that I could explore further.
|
||||
|
||||
[swift]: https://belkadan.com/blog/2020/04/Swift-on-Mac-OS-9/
|
||||
[BlueSCSI]: https://github.com/erichelgeson/BlueSCSI
|
||||
[Retro68]: https://github.com/autc04/Retro68
|
||||
[SheepShaver]: https://sheepshaver.cebix.net/
|
||||
[Dialog sample]: https://github.com/autc04/Retro68/tree/5f882506013a0a8a4335350197a1b7c91763494e/Samples/Dialog
|
||||
[Pascal string]: https://en.wikipedia.org/wiki/String_(computer_science)#Length-prefixed
|
||||
[libgccjit]: https://gcc.gnu.org/onlinedocs/jit/
|
221
v2/content/posts/2023/systemd-gdb-coredumps.md
Normal file
|
@ -0,0 +1,221 @@
|
|||
+++
|
||||
title = "Debugging a Docker Core Dump"
|
||||
date = 2023-02-25T10:39:49+10:00
|
||||
|
||||
#[extra]
|
||||
#updated = 2023-01-11T21:11:28+10:00
|
||||
+++
|
||||
|
||||
On my main machine I use an excellent cross-platform tool called [Docuum] that
|
||||
automatically cleans up unused docker images. This allows me to use Docker
|
||||
without the need to periodically wonder why I'm out of disk space, run `docker
|
||||
system prune` and recover half my disk.
|
||||
|
||||
I installed Docuum via [the AUR package][aurpkg] (although tweaked to build the
|
||||
latest Docuum release) and ran it via the bundled systemd service definition.
|
||||
This worked great for a while but some time back it started failing. Every time
|
||||
Docuum would try to check for things to clean up I'd see the following in the
|
||||
system journal:
|
||||
|
||||
<!-- more -->
|
||||
|
||||
```
|
||||
Feb 25 10:03:12 ryzen docuum[77751]: [2023-02-25 10:03:12 +10:00 INFO] Performing an initial vacuum on startup…
|
||||
Feb 25 10:03:12 ryzen kernel: audit: type=1326 audit(1677283392.831:2): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=77763 comm="docker" exe="/usr/bin/docke>
|
||||
Feb 25 10:03:12 ryzen systemd[1]: Created slice Slice /system/systemd-coredump.
|
||||
Feb 25 10:03:12 ryzen systemd[1]: Started Process Core Dump (PID 77768/UID 0).
|
||||
Feb 25 10:03:13 ryzen systemd-coredump[77769]: [🡕] Process 77763 (docker) of user 0 dumped core.
|
||||
|
||||
Stack trace of thread 77763:
|
||||
#0 0x00005568dbcb5c4e n/a (docker + 0x243c4e)
|
||||
#1 0x00005568dbd35a3b n/a (docker + 0x2c3a3b)
|
||||
#2 0x00005568dbd3482f n/a (docker + 0x2c282f)
|
||||
#3 0x00005568dbd6c2ee n/a (docker + 0x2fa2ee)
|
||||
#4 0x00005568dbcfafa8 n/a (docker + 0x288fa8)
|
||||
#5 0x00005568dbcfaef1 n/a (docker + 0x288ef1)
|
||||
#6 0x00005568dbced953 n/a (docker + 0x27b953)
|
||||
#7 0x00005568dbd1eb41 n/a (docker + 0x2acb41)
|
||||
ELF object binary architecture: AMD x86-64
|
||||
Feb 25 10:03:13 ryzen docuum[77751]: [2023-02-25 10:03:13 +10:00 ERROR] Unable to list images.
|
||||
Feb 25 10:03:13 ryzen docuum[77751]: [2023-02-25 10:03:13 +10:00 INFO] Retrying in 5 seconds…
|
||||
Feb 25 10:03:13 ryzen systemd[1]: systemd-coredump@0-77768-0.service: Deactivated successfully.
|
||||
```
|
||||
|
||||
This would repeat every 5 seconds. I ignored this for a while but finally decided
|
||||
to investigate it today. To find the failing command I ran
|
||||
`coredumpctl list` then identified one of the docker crashes and ran
|
||||
`coredumpctl info` with its PID:
|
||||
|
||||
```
|
||||
$ coredumpctl info 78255
|
||||
PID: 78255 (docker)
|
||||
UID: 0 (root)
|
||||
GID: 0 (root)
|
||||
Signal: 31 (SYS)
|
||||
Timestamp: Sat 2023-02-25 10:03:23 AEST (44min ago)
|
||||
Command Line: docker image ls --all --no-trunc --format $'{{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}'
|
||||
Executable: /usr/bin/docker
|
||||
Control Group: /system.slice/docuum.service
|
||||
Unit: docuum.service
|
||||
Slice: system.slice
|
||||
Boot ID: 0ac9f0dd246548949c3a90a0e7494665
|
||||
Machine ID: affcb0b7a7d1464385d65464d9be450e
|
||||
Hostname: ryzen
|
||||
Storage: /var/lib/systemd/coredump/core.docker.0.0ac9f0dd246548949c3a90a0e7494665.78255.1677283403000000.zst (inaccessible)
|
||||
Message: Process 78255 (docker) of user 0 dumped core.
|
||||
```
|
||||
|
||||
Strangely I could run the command myself (`Command Line` line) just fine. I
|
||||
figured I needed to see where in docker it was crashing. I learned how to
|
||||
access systemd coredumps with gdb and ran: `sudo coredumpctl gdb 78255`. `sudo`
|
||||
is needed because the core dump belongs to root due the crashing `docker`
|
||||
process belonging to root. This didn't yield much extra info as debug symbols
|
||||
were not present for the binary. It did identify why it crashed though:
|
||||
|
||||
> Program terminated with signal SIGSYS, Bad system call.
|
||||
|
||||
Knowing that docker was implemented in Go and that they make system calls
|
||||
manually on Linux I wondered if this was some sort of Go bug—although given
|
||||
the popularity of Docker this did seem unlikely.
|
||||
|
||||
To get more info I needed debug symbols. Arch Linux makes these available via
|
||||
[debuginfod](https://wiki.archlinux.org/title/Debuginfod) and gdb can
|
||||
automatically download debuginfo files if `DEBUGINFOD_URLS` is set. I reran
|
||||
`gdb`, telling `sudo` to pass the `DEBUGINFOD_URLS` environment variable
|
||||
through (I had already set `DEBUGINFOD_URLS=https://debuginfod.archlinux.org/`
|
||||
in my Zsh config some time ago):
|
||||
|
||||
```
|
||||
sudo --preserve-env=DEBUGINFOD_URLS coredumpctl gdb 78255
|
||||
```
|
||||
|
||||
Now there was a proper backtrace:
|
||||
|
||||
```
|
||||
#0 runtime/internal/syscall.Syscall6 () at runtime/internal/syscall/asm_linux_amd64.s:36
|
||||
#1 0x00005588e7b93c33 in syscall.RawSyscall6 (num=160, a1=7, a2=94046491589710, a3=7, a4=824634289408, a5=0, a6=0, r1=<optimized out>, r2=<optimized out>,
|
||||
errno=<optimized out>) at runtime/internal/syscall/syscall_linux.go:38
|
||||
#2 0x00005588e7c13a3b in syscall.RawSyscall (trap=160, a1=7, a2=94046491589710, a3=7, r1=<optimized out>, r2=<optimized out>, err=<optimized out>)
|
||||
at syscall/syscall_linux.go:62
|
||||
#3 0x00005588e7c1282f in syscall.Setrlimit (resource=<optimized out>, rlim=<optimized out>, err=...) at syscall/zsyscall_linux_amd64.go:1326
|
||||
#4 0x00005588e7c4a2ee in os.init.1 () at os/rlimit.go:30
|
||||
#5 0x00005588e7bd8fa8 in runtime.doInit (t=0x5588e91a7be0 <os.[inittask]>) at runtime/proc.go:6506
|
||||
#6 0x00005588e7bd8ef1 in runtime.doInit (t=0x5588e91a9900 <main.[inittask]>) at runtime/proc.go:6483
|
||||
#7 0x00005588e7bcb953 in runtime.main () at runtime/proc.go:233
|
||||
#8 0x00005588e7bfcb41 in runtime.goexit () at runtime/asm_amd64.s:1598
|
||||
#9 0x0000000000000000 in ?? ()
|
||||
```
|
||||
|
||||
So the issue seems to be a call to `setrlimit`. Looking at [the code][gocode]
|
||||
and searching the Go issue tracker didn't turn up anyone else having this
|
||||
issue, which pointed at an issue on my system.
|
||||
|
||||
I'm honestly not sure what led me to the next step but I decided to take a look
|
||||
at the Docuum service definition. I was surprised to see that it was more
|
||||
complicated than most definitions I'm used to seeing:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=LRU eviction of Docker images
|
||||
Documentation=https://github.com/stepchowfun/docuum
|
||||
DefaultDependencies=false
|
||||
After=docker.service docker.socket
|
||||
Requires=docker.service docker.socket
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=DOCUUM_THRESHOLD=10GB
|
||||
EnvironmentFile=-/etc/default/docuum
|
||||
ExecStart=/usr/bin/docuum --threshold $DOCUUM_THRESHOLD
|
||||
ProtectSystem=full
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
PrivateNetwork=true
|
||||
CapabilityBoundingSet=
|
||||
KeyringMode=private
|
||||
RestrictNamespaces=~cgroup ipc net mnt pid user uts
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
ReadWritePaths=/var/run/docker.sock
|
||||
DeviceAllow=
|
||||
IPAddressDeny=any
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
PrivateMounts=true
|
||||
PrivateUsers=true
|
||||
ProtectControlGroups=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=tmpfs
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
UMask=0077
|
||||
ProtectHostname=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Suspiciously it seemed to be doing some sandboxing and filtering system calls
|
||||
(`SystemCallFilter`). A bit more research pointed me to `systemd-analyze
|
||||
syscall-filter`, which lists which system calls belong to the predefined system
|
||||
call sets (`@privileged`, `@resources`, etc.).
|
||||
|
||||
`setrlimit` was listed under `@resources`:
|
||||
|
||||
```
|
||||
@resources
|
||||
# Alter resource settings
|
||||
ioprio_set
|
||||
mbind
|
||||
migrate_pages
|
||||
move_pages
|
||||
nice
|
||||
sched_setaffinity
|
||||
sched_setattr
|
||||
sched_setparam
|
||||
sched_setscheduler
|
||||
set_mempolicy
|
||||
setpriority
|
||||
setrlimit
|
||||
```
|
||||
|
||||
The [systemd docs for `SystemCallFilter`][SystemCallFilter] also mentioned:
|
||||
|
||||
> If the first character of the list is "~", the effect is inverted: only the
|
||||
> listed system calls will result in immediate process termination
|
||||
> (deny-listing)
|
||||
|
||||
So we finally had our culprit: the service definition was denying system calls in
|
||||
the `@resources` set and at some point `docker` had started making `setrlimit` calls,
|
||||
which were resulting in termination.
|
||||
|
||||
The fix was simple enough: I removed `@resources` from the deny list, rebuilt
|
||||
the package, and then re-enabled the `docuum` service (I'd previously disabled
|
||||
it do to the constant crashes). I was pleased to see it start successfully and
|
||||
begin vacuuming up a few months of Docker image detritus.
|
||||
|
||||
### Conclusion
|
||||
|
||||
This small debugging session taught me a number of things: I learned how to
|
||||
find and list core dumps managed by systemd, how to open them in GDB with
|
||||
symbols present, and that systemd has powerful, fine-grained system call
|
||||
sandboxing.
|
||||
|
||||
I was ultimately able to resolve the issue and get Docuum working again.
|
||||
I have published my patched version of the AUR package to my personal AUR
|
||||
repo in case it's useful to anyone else:
|
||||
|
||||
<https://github.com/wezm/aur/tree/master/docuum>.
|
||||
|
||||
[aurpkg]: https://aur.archlinux.org/packages/docuum
|
||||
[Docuum]: https://github.com/stepchowfun/docuum
|
||||
[gocode]: https://github.com/golang/go/blob/203e59ad41bd288e1d92b6f617c2f55e70d3c8e3/src/syscall/zsyscall_linux_amd64.go#L1335
|
||||
[service]: https://aur.archlinux.org/cgit/aur.git/tree/docuum.service?h=docuum&id=650d2c24fe9df712e8a98dde37f3ee47d3af4e47
|
||||
[SystemCallFilter]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html#System%20Call%20Filtering
|
179
v2/content/posts/2023/xslt-podcast/index.md
Normal file
|
@ -0,0 +1,179 @@
|
|||
+++
|
||||
title = "Creating a Podcast From a Mastodon Account With XSLT"
|
||||
date = 2023-03-01T18:33:33+10:00
|
||||
|
||||
[extra]
|
||||
updated = 2023-03-01T21:54:06+10:00
|
||||
+++
|
||||
|
||||
{% aside(title="Just want the feed?", float="right") %}
|
||||
Here you go:<br>
|
||||
[ATPrewind podcast feed](https://files.wezm.net/atprewind.rss)
|
||||
{% end %}
|
||||
|
||||
I recently discovered the [ATPrewind account on Mastodon][ATPrewind]. It's an account
|
||||
sharing "gems discovered while re-listening to [@atpfm] from the very first
|
||||
episode. By [@joshua]". ATP is a tech Podcast that's been running for about 10
|
||||
years. Each post (so far) from ATPrewind includes a short clip from the show in the
|
||||
form of a little video.
|
||||
|
||||
This post describes how I was nerd sniped into creating a podcast from the ATPrewind posts.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
It all started when [I posted the following][my-post] on Mastodon:
|
||||
|
||||
> Ahh this ATP Rewind account is gold https://social.tupo.space/@ATPrewind
|
||||
>
|
||||
> Keep up the great work @joshua
|
||||
|
||||
[Kashyap replied][Kashyap]:
|
||||
|
||||
> Indeed! This should also be a podcast 🙃
|
||||
|
||||
This was a great idea and it got me thinking about how to do it with the least
|
||||
amount of effort.
|
||||
|
||||
In this day and age I imagine many programmers would reach for
|
||||
their favourite programming language and code up something to generate a
|
||||
podcast feed (real podcasts are just RSS). Perhaps using the Mastodon API or
|
||||
similar and then work out a way to host their program.
|
||||
|
||||
With [my recent experience with Deno Deploy][deno-deploy] fresh in my mind I
|
||||
considered using it. However I opted for a decidedly late 90s solution: [XSLT].
|
||||
According to Wikipedia "XSLT is a language originally designed for transforming
|
||||
XML documents into other XML documents". The 'originally' refers to fact that
|
||||
you can now generate any text with it, not just XML.
|
||||
Since it was created in the era of "XML ALL THE THINGS", XSL templates are
|
||||
themselves XML documents. It also makes extensive use of [XPath] expressions to
|
||||
select nodes and extract their content.
|
||||
|
||||
### Creating a Podcast Feed
|
||||
|
||||
Every Mastodon account has an RSS feed so I created an XSL template to process
|
||||
the ATPrewind RSS feed and add the missing elements required to turn it into a
|
||||
valid podcast feed. With some help from [this Dr. Drang][drdrang] post this is
|
||||
what I came up with:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:media="http://search.yahoo.com/mrss/">
|
||||
|
||||
<xsl:output method="xml" encoding="utf-8" indent="yes"/>
|
||||
|
||||
<!-- First, get everything. -->
|
||||
<xsl:template match="node() | @*">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="node() | @*"/>
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
<!-- Update items to include title and enclosure elements. -->
|
||||
<xsl:template match="/rss/channel/item">
|
||||
<item>
|
||||
<xsl:apply-templates select="node()" />
|
||||
<title>Post on <xsl:value-of select="pubDate"/></title>
|
||||
<enclosure url="{media:content/@url}" length="{media:content/@fileSize}" type="audio/mp4; codecs="mp4a.40.2"" />
|
||||
</item>
|
||||
</xsl:template>
|
||||
|
||||
</xsl:stylesheet>
|
||||
```
|
||||
|
||||
XML noise aside this document should be fairly self explanatory. It copies all
|
||||
nodes from the source RSS feed, then processes each `item` element. To each one
|
||||
it adds a `title` element derived from the text "Post on" and the publication
|
||||
date of the post, and an `enclosure` element derived from the `media:content`
|
||||
element.
|
||||
|
||||
I lie a bit by saying that the enclosure MIME type is `audio/mp4;
|
||||
codecs="mp4a.40.2"`. I.e. AAC-LC in MP4 container for the video in each post.
|
||||
Running `curl -L https://social.tupo.space/@ATPrewind.rss | xsltproc
|
||||
podcast.xsl` produces the podcast feed.
|
||||
|
||||
It took a few tries to get it to work but eventually I was able to convince
|
||||
[Overcast] that it was a real podcast. One trick that I'm exploiting here is
|
||||
that the MP4 container for video and audio is the same, the audio only version
|
||||
is just lacking the video stream. I figured that podcast players might still be
|
||||
able to play the video and at least for Overcast it works:
|
||||
|
||||
{{ figure(image="posts/2023/xslt-podcast/overcast-screenshot.png", link="posts/2023/xslt-podcast/overcast-screenshot.png", alt="Screenshot of Overcast showing the podcast 'episodes'.", caption="Screenshot of Overcast showing the 'episodes'.", width=393) }}
|
||||
|
||||
|
||||
### Deployment
|
||||
|
||||
To deploy this contraption I created a Docker image with `curl` and `libxslt`
|
||||
installed:
|
||||
|
||||
```dockerfile
|
||||
FROM wezm-alpine:3.17.2
|
||||
|
||||
# UID needs to match owner of /home/rss/feeds volume
|
||||
ARG PUID=1000
|
||||
ARG PGID=1000
|
||||
ARG USER=rss
|
||||
|
||||
RUN addgroup -g ${PGID} ${USER} && \
|
||||
adduser -D -u ${PUID} -G ${USER} -h /home/${USER} -D ${USER}
|
||||
|
||||
RUN apk --update add curl libxslt
|
||||
|
||||
COPY ./entrypoint.sh /home/${USER}/entrypoint.sh
|
||||
COPY ./podcast.xsl /home/${USER}/podcast.xsl
|
||||
|
||||
WORKDIR /home/${USER}
|
||||
|
||||
USER ${USER}
|
||||
|
||||
VOLUME ["/home/rss/feeds"]
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
```
|
||||
|
||||
I made a small script as the entrypoint to the container:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
trap 'exit' TERM INT
|
||||
|
||||
while true; do
|
||||
curl -L https://social.tupo.space/@ATPrewind.rss | xsltproc podcast.xsl - > /home/rss/feeds/atprewind.rss
|
||||
sleep 3600 # 1 hour
|
||||
done
|
||||
```
|
||||
|
||||
It regenerates the podcast feed each hour. I bind mount the directory for
|
||||
files.wezm.net into the container in my Docker Compose config, which takes
|
||||
advantage of the fact that nginx is already serving that directory.
|
||||
|
||||
Some may argue that the Docker part of this is not in the spirit of, "least
|
||||
amount of effort", but I already have all this set up on my server so adding
|
||||
one more container is very little effort. I've written previously about [my
|
||||
Alpine Linux server][server] if you'd like to read more.
|
||||
|
||||
Once I pushed the Docker image the podcast was live. The URL is:<br>
|
||||
<https://files.wezm.net/atprewind.rss> if you'd like to subscribe.
|
||||
|
||||
### Future Work
|
||||
|
||||
I whipped all this up before work today and made an assumption that might not
|
||||
always hold: there is a single video media attachment on each post. So far
|
||||
that's true but I should update the XSL template to only try to generate an
|
||||
`enclosure` element if the attachment is present. It would also be a good idea
|
||||
to filter for audio and video only in case a post appears with images.
|
||||
|
||||
For now I shall eagerly look forward to the next post appearing in Overcast.
|
||||
|
||||
[@atpfm]: https://mastodon.social/@atpfm
|
||||
[@joshua]: https://social.tupo.space/@joshua
|
||||
[ATPrewind]: https://social.tupo.space/@ATPrewind
|
||||
[Overcast]: https://overcast.fm/
|
||||
[XPath]: https://www.w3.org/TR/xpath-31/
|
||||
[XSLT]: https://www.w3.org/TR/xslt-30/
|
||||
[deno-deploy]: https://www.youtube.com/watch?v=d-tsfUVg4II
|
||||
[drdrang]: https://leancrew.com/all-this/2022/08/filtering-my-rss-reading/
|
||||
[server]: https://www.wezm.net/technical/2019/02/alpine-linux-docker-infrastructure/
|
||||
[my-post]: https://mastodon.decentralised.social/@wezm/109940341596949214
|
||||
[Kashyap]: https://mastodon.social/@kgrz/109942702497796855
|
BIN
v2/content/posts/2023/xslt-podcast/overcast-screenshot.png
Normal file
After Width: | Height: | Size: 191 KiB |
|
@ -183,6 +183,9 @@ figure {
|
|||
.figure-border img {
|
||||
border: 1px solid #AAA;
|
||||
}
|
||||
.figure-pixelated img {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
figcaption {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
@ -360,6 +363,20 @@ ul.projects {
|
|||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
.no-overlay {
|
||||
position: relative;
|
||||
}
|
||||
.no-overlay::after {
|
||||
content: '🚫';
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-size: 96pt;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.socials {
|
||||
margin-top: 2em;
|
||||
|
||||
|
@ -373,6 +390,9 @@ ul.projects {
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.mastodon-embed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.tagline {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<div class="flex">
|
||||
<section class="posts-section">
|
||||
<h2>
|
||||
Recent Posts <a href="https://twitter.com/wezm?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">Follow @wezm</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
Recent Posts
|
||||
</h2>
|
||||
|
||||
{% set section = get_section(path="posts/_index.md") %}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<a href="{{ config.base_url }}/rss.xml">RSS</a>
|
||||
<a href="mailto:{{ config.extra.email }}">Email</a>
|
||||
<a href="https://twitter.com/wezm">Twitter</a>
|
||||
<a href="https://decentralised.social/wezm">Fediverse</a>
|
||||
<a href="https://mastodon.decentralised.social/@wezm" rel="me">Fediverse</a>
|
||||
<a href="https://github.com/wezm">GitHub</a>
|
||||
<a href="https://github.com/sponsors/wezm">Support My Work</a>
|
||||
</div>
|
||||
|
|
|
@ -61,7 +61,11 @@
|
|||
</svg>
|
||||
Stay in touch!
|
||||
</h3>
|
||||
<p> Follow me on <a href="https://twitter.com/wezm">Twitter</a> or the <a href="https://decentralised.social/wezm">Fediverse</a>, <a href="{{ config.base_url }}/rss.xml">subscribe to the feed</a>, or <a href="mailto:wes@wezm.net">send me an email</a>.
|
||||
<p>
|
||||
Follow me on the <a href="https://mastodon.decentralised.social/@wezm">Fediverse</a>
|
||||
(or <a href="https://twitter.com/wezm">Twitter</a>),
|
||||
<a href="{{ config.base_url }}/rss.xml">subscribe to the feed</a>,
|
||||
or <a href="mailto:wes@wezm.net">send me an email</a>.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
{% set figure_class = ["text-center"] %}
|
||||
{% if border %}
|
||||
<figure class="text-center figure-border">
|
||||
{% else %}
|
||||
<figure class="text-center">
|
||||
{% set figure_class = figure_class | concat(with="figure-border") %}
|
||||
{% endif %}
|
||||
{% if pixelated %}
|
||||
{% set figure_class = figure_class | concat(with="figure-pixelated") %}
|
||||
{% endif %}
|
||||
<figure class="{{ figure_class | join(sep=" ") }}">
|
||||
<a href="{{ config.base_url }}/{{ link }}">
|
||||
{% if resize_width %}
|
||||
{% set image = resize_image(path=image, width=resize_width, op="fit_width", quality=quality | default(value=75)) %}
|
||||
|
|
26
v2/templates/shortcodes/figure_no.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{% if border %}
|
||||
<figure class="text-center figure-border no-overlay">
|
||||
{% else %}
|
||||
<figure class="text-center no-overlay">
|
||||
{% endif %}
|
||||
{% if resize_width %}
|
||||
{% set image = resize_image(path=image, width=resize_width, op="fit_width", quality=quality | default(value=75)) %}
|
||||
{% if width %}
|
||||
<img src="{{ image.url }}" width="{{ width }}" alt="{{ alt }}" />
|
||||
{% else %}
|
||||
<img src="{{ image.url }}" alt="{{ alt }}" />
|
||||
{% endif %}
|
||||
{% elif resize_height %}
|
||||
{% set image = resize_image(path=image, height=resize_height, op="fit_height", quality=quality | default(value=75)) %}
|
||||
{% if height %}
|
||||
<img src="{{ image.url }}" style="max-height: {{ height }}px" alt="{{ alt }}" />
|
||||
{% else %}
|
||||
<img src="{{ image.url }}" alt="{{ alt }}" />
|
||||
{% endif %}
|
||||
{% elif width %}
|
||||
<img src="{{ config.base_url }}/{{ image }}" width="{{ width }}" alt="{{ alt }}" />
|
||||
{% else %}
|
||||
<img src="{{ config.base_url }}/{{ image }}" alt="{{ alt }}" />
|
||||
{% endif %}
|
||||
<figcaption>{{ caption }}</figcaption>
|
||||
</figure>
|
|
@ -1,4 +1,13 @@
|
|||
{% if poster %}
|
||||
{% else %}
|
||||
{% set poster = "jpg" %}
|
||||
{% endif %}
|
||||
{% if preload %}
|
||||
{% else %}
|
||||
{% set preload = "none" %}
|
||||
{% endif %}
|
||||
|
||||
<figure class="text-center">
|
||||
<video controls preload="none" src="{{ config.base_url }}/{{ video }}" poster="{{ config.base_url }}/{{ video }}.jpg" style="max-height: {{ height }}px"></video>
|
||||
<video controls preload="{{ preload }}" src="{{ config.base_url }}/{{ video }}" poster="{{ config.base_url }}/{{ video }}.{{ poster }}" style="max-height: {{ height }}px" aria-label="{{ alt }}"></video>
|
||||
<figcaption>{{ caption }}</figcaption>
|
||||
</figure>
|
||||
|
|