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.
|
# same as the items root, but applies to layouts rather than items.
|
||||||
layouts_root: /
|
layouts_root: /
|
||||||
|
|
||||||
base_url: 'http://www.wezm.net'
|
base_url: 'https://www.wezm.net'
|
||||||
deploy:
|
deploy:
|
||||||
default:
|
default:
|
||||||
kind: rsync
|
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>
|
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="http://www.flickr.com/photos/wezm/">Flickr</a></li>
|
||||||
<li><a href="https://github.com/wezm">GitHub</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="https://patreon.com/wezm">Patreon</a></li>
|
||||||
<li><a href="http://stackoverflow.com/users/38820/wes">Stack Overflow</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>
|
<li><a href="https://twitter.com/wezm">Twitter</a></li>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
User-Agent: *
|
User-Agent: *
|
||||||
Disallow:
|
Disallow:
|
||||||
Sitemap: <%= @config[:base_url] %>/sitemap.xml
|
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.
|
* [Jeremy Soller] is creating [Redox OS], an MIT licensed OS written in Rust.
|
||||||
$60
|
$60
|
||||||
* [Gargron] is creating the [Mastodon] federated social network. You can find
|
* [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
|
* [Jorge Aparicio] is doing fantastic work building Rust tooling and pushing
|
||||||
the state of the art of [Rust on microcontrollers][embedded-rust]. $35
|
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].
|
* [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>
|
<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>
|
</svg>
|
||||||
</a>
|
</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>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<img src="/images/comment.svg" width="32" height="32" alt="Comment icon" class="comment align-top" />
|
<img src="/images/comment.svg" width="32" height="32" alt="Comment icon" class="comment align-top" />
|
||||||
Stay in touch!
|
Stay in touch!
|
||||||
</h2>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -8,3 +8,7 @@ deploy: build
|
||||||
# build with zola
|
# build with zola
|
||||||
build:
|
build:
|
||||||
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
|
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
|
small microcontrollers, up to large servers and the operating systems that run
|
||||||
upon them. I'm a [Rustacean] {{ quiet(text="🦀") }} with a fondness for
|
upon them. I'm a [Rustacean] {{ quiet(text="🦀") }} with a fondness for
|
||||||
mechanical keyboards. I work at [YesLogic] on the [Prince] HTML to PDF
|
mechanical keyboards. [Read more on the about page →](@/about.md)
|
||||||
converter. [Read more on the about page →](@/about.md)
|
|
||||||
|
|
||||||
[Prince]: https://www.princexml.com/
|
[Prince]: https://www.princexml.com/
|
||||||
[Rustacean]: https://www.rust-lang.org/learn/get-started#ferris
|
[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
|
community and curate [Read Rust], a website that collects interesting posts
|
||||||
from the Rust community.
|
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.
|
I am fine with either he/him or they/them pronouns.
|
||||||
|
|
||||||
[Alpine Linux]: https://alpinelinux.org/
|
[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 desktop]: https://bitcannon.net/page/ryzen9-pc/
|
||||||
[my laptop]: https://bitcannon.net/post/huawei-matebook-x-pro-void-linux/
|
[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/
|
[my server]: https://www.wezm.net/technical/2019/02/alpine-linux-docker-infrastructure/
|
||||||
[Prince]: https://www.princexml.com/
|
|
||||||
[Read Rust]: https://readrust.net/
|
[Read Rust]: https://readrust.net/
|
||||||
[Rust]: https://www.rust-lang.org/
|
[Rust]: https://www.rust-lang.org/
|
||||||
[Void Linux]: https://voidlinux.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
|
#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
|
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
|
I looked I only used a small fraction of this allocation across the various
|
||||||
[things I host][alpine-docker].
|
[things I host][alpine-docker].
|
||||||
|
@ -75,3 +75,4 @@ my peace with linking to Twitter.
|
||||||
[nitter-instance]: https://decentralised.social/notice/A41E2cjuM14UYFAF7o
|
[nitter-instance]: https://decentralised.social/notice/A41E2cjuM14UYFAF7o
|
||||||
[robots.txt]: https://github.com/wezm/nitter/commit/4e7bd7b8853bf36008a3d1e79ee97deaa68743da
|
[robots.txt]: https://github.com/wezm/nitter/commit/4e7bd7b8853bf36008a3d1e79ee97deaa68743da
|
||||||
[Varnish]: https://varnish-cache.org/
|
[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
|
date = 2022-04-20T06:38:27+10:00
|
||||||
|
|
||||||
[extra]
|
[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
|
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
|
All the code, hardware designs, and Buildroot configuration/overlay is
|
||||||
available in [the git repository][repo] if you're interested.
|
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
|
[app]: https://github.com/wezm/garage-door-monitor/tree/main/app
|
||||||
[Awesome]: https://github.com/awesomeWM/awesome
|
[Awesome]: https://github.com/awesomeWM/awesome
|
||||||
[FreeCAD]: https://www.freecadweb.org/
|
[FreeCAD]: https://www.freecadweb.org/
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
title = "Fixing Monospace Text in Kobo eReaders"
|
title = "Fixing Monospace Text in Kobo eReaders"
|
||||||
date = 2022-04-10T09:14:50+10:00
|
date = 2022-04-10T09:14:50+10:00
|
||||||
|
|
||||||
# [extra]
|
[extra]
|
||||||
# updated = 2022-01-27T21:07:32+10:00
|
updated = 2022-08-22T14:10:50+10:00
|
||||||
+++
|
+++
|
||||||
|
|
||||||
After verifying with friends that eBook readers do a decent job of rendering
|
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 -->
|
<!-- 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)
|
[Skip to Instructions ⮷](#instructions)
|
||||||
|
|
||||||
I quickly discovered this is a known issue with Kobo readers dating back years:
|
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 {
|
.figure-border img {
|
||||||
border: 1px solid #AAA;
|
border: 1px solid #AAA;
|
||||||
}
|
}
|
||||||
|
.figure-pixelated img {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
figcaption {
|
figcaption {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -360,6 +363,20 @@ ul.projects {
|
||||||
margin-bottom: 3em;
|
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 {
|
.socials {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
|
|
||||||
|
@ -373,6 +390,9 @@ ul.projects {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.mastodon-embed {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
.tagline {
|
.tagline {
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<section class="posts-section">
|
<section class="posts-section">
|
||||||
<h2>
|
<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>
|
</h2>
|
||||||
|
|
||||||
{% set section = get_section(path="posts/_index.md") %}
|
{% set section = get_section(path="posts/_index.md") %}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<a href="{{ config.base_url }}/rss.xml">RSS</a>
|
<a href="{{ config.base_url }}/rss.xml">RSS</a>
|
||||||
<a href="mailto:{{ config.extra.email }}">Email</a>
|
<a href="mailto:{{ config.extra.email }}">Email</a>
|
||||||
<a href="https://twitter.com/wezm">Twitter</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/wezm">GitHub</a>
|
||||||
<a href="https://github.com/sponsors/wezm">Support My Work</a>
|
<a href="https://github.com/sponsors/wezm">Support My Work</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -61,7 +61,11 @@
|
||||||
</svg>
|
</svg>
|
||||||
Stay in touch!
|
Stay in touch!
|
||||||
</h3>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
{% set figure_class = ["text-center"] %}
|
||||||
{% if border %}
|
{% if border %}
|
||||||
<figure class="text-center figure-border">
|
{% set figure_class = figure_class | concat(with="figure-border") %}
|
||||||
{% else %}
|
|
||||||
<figure class="text-center">
|
|
||||||
{% endif %}
|
{% 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 }}">
|
<a href="{{ config.base_url }}/{{ link }}">
|
||||||
{% if resize_width %}
|
{% if resize_width %}
|
||||||
{% set image = resize_image(path=image, width=resize_width, op="fit_width", quality=quality | default(value=75)) %}
|
{% 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">
|
<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>
|
<figcaption>{{ caption }}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|