domm.plix.at.atom.xml - sfeed_tests - sfeed tests and RSS and Atom files
 (HTM) git clone git://git.codemadness.org/sfeed_tests
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
       domm.plix.at.atom.xml (37839B)
       ---
            1 <?xml version="1.0" encoding="us-ascii"?>
            2 <feed xmlns="http://www.w3.org/2005/Atom"><title>domm.plix.at</title><author><name>Thomas Klausner</name></author><link href="https://domm.plix.at/index.xml" rel="self"/><id>https://domm.plix.at/index.xml</id><updated>2025-10-06T15:00:00+00:00</updated><generator uri="https://metacpan.org/pod/XML::Atom::SimpleFeed" version="0.905">XML::Atom::SimpleFeed</generator><entry><title>I brain coded a static image gallery in a few hours: snig.pl</title><link href="https://domm.plix.at/perl/2025_10_braincoded_static_image_gallery.html"/><id>https://domm.plix.at/perl/2025_10_braincoded_static_image_gallery.html</id><updated>2025-10-06T15:00:00+00:00</updated><category term="perl"/><summary>For quite some I wanted to write a small static image gallery so I can share my pictures with friends and family. Of course there are a gazillion tools like this, but, well, sometimes I just want to ...</summary><content type="html">&lt;p&gt;For quite some I wanted to write a small static image gallery so I can share my pictures with friends and family. Of course there are a gazillion tools like this, but, well, sometimes I just want to roll my own.&lt;/p&gt;
            3 
            4 &lt;p&gt;I took the opportunity during our &lt;a href=&#34;/reisen/2025_schwaz.html&#34;&gt;stay in Schwaz&lt;/a&gt; to take a few hours and hack together &lt;a href=&#34;https://snig.plix.at/&#34;&gt;snig&lt;/a&gt;, the (small | static | simple | stupid | ...) image gallery. &lt;a href=&#34;https://snig.plix.at/pub/202509_a_few_days_in_tyrol/&#34;&gt;Here&lt;/a&gt; you can see the example gallery (showing some of the pictures I took in Schwaz).&lt;/p&gt;
            5 
            6 &lt;p&gt;I used the old, well tested technique I call &lt;b&gt;brain coding&lt;/b&gt;&lt;sup class=&#34;footnote&#34;&gt;&lt;a href=&#34;#fn0&#34;&gt;0&lt;/a&gt;&lt;/sup&gt;, where you start with an empty vim buffer and type some code (Perl, &lt;span class=&#34;caps&#34;&gt;HTML, CSS&lt;/span&gt;) until you&#39;re happy with the result. It helps to think a bit (aka use your brain) during this process.&lt;/p&gt;
            7 
            8 &lt;p&gt;According to my &lt;a href=&#34;http://timetracker.plix.at/&#34;&gt;timetracker&lt;/a&gt; I spend 8h 15min (probably half of it spend fiddling with &lt;span class=&#34;caps&#34;&gt;CSS...&lt;/span&gt;).&lt;/p&gt;
            9 
           10 &lt;h3&gt;Installation&lt;/h3&gt;
           11 
           12 &lt;p&gt;I used the new &lt;a href=&#34;https://perldoc.perl.org/perlclass&#34;&gt;Perl class&lt;/a&gt; feature, so you&#39;ll need at least Perl 5.40 which was released last year and is included in current Debian.&lt;/p&gt;
           13 
           14 &lt;p&gt;I prefer &lt;a href=&#34;https://metacpan.org/pod/App::cpm&#34;&gt;cpm&lt;/a&gt; to install &lt;span class=&#34;caps&#34;&gt;CPAN &lt;/span&gt;modules:&lt;/p&gt;
           15 
           16 &lt;pre&gt;&lt;code&gt;cpm install -g Snig&lt;/code&gt;&lt;/pre&gt;
           17 
           18 &lt;p&gt;I haven&#39;t provided a &lt;code&gt;Containerfile&lt;/code&gt; yet, but if somebody is interested, drop me a line.&lt;/p&gt;
           19 
           20 &lt;p&gt;You can get the raw source code from &lt;a href=&#34;https://git.sr.ht/~domm/snig&#34;&gt;Source Hut&lt;/a&gt; (as I don&#39;t want to support the big &lt;span class=&#34;caps&#34;&gt;LLM &lt;/span&gt;vacuum machines formerly known as Git(Hub|Lab)).&lt;/p&gt;
           21 
           22 &lt;h3&gt;Example usage&lt;/h3&gt;
           23 
           24 &lt;p&gt;You need a folder filled with images (eg &lt;code&gt;some-pictures&lt;/code&gt;) and some place where you can host a folder and a bunch of &lt;span class=&#34;caps&#34;&gt;HTML &lt;/span&gt;files.&lt;/p&gt;
           25 
           26 &lt;pre&gt;&lt;code&gt;ls -la some-pictures/
           27 7156 -rw------- 1 domm domm 7322112 Oct  6 09:14 P1370198.JPG
           28 7188 -rw------- 1 domm domm 7354880 Oct  6 09:14 P1370208.JPG
           29 7204 -rw------- 1 domm domm 7369728 Oct  6 09:14 P1370257.JPG&lt;/code&gt;&lt;/pre&gt;
           30 
           31 &lt;p&gt;Then you do&lt;/p&gt;
           32 
           33 &lt;pre&gt;&lt;code&gt;snig.pl --input some-pictures --output /var/web/my-server-net/gallery/2025-10-some-pictures --name &#38;quot;Some nice pictures&#38;quot;&lt;/code&gt;&lt;/pre&gt;
           34 
           35 &lt;p&gt;This will:&lt;/p&gt;
           36 
           37 &lt;ul&gt;
           38 &lt;li&gt;find all &lt;code&gt;jpgs&lt;/code&gt; in the folder &lt;code&gt;some-pictures&lt;/code&gt;&lt;/li&gt;
           39 &lt;li&gt;copy them into the output folder&lt;/li&gt;
           40 &lt;li&gt;generate a thumbnail (for use in the list)&lt;/li&gt;
           41 &lt;li&gt;generate a preview (for use in the detail page)&lt;/li&gt;
           42 &lt;li&gt;generate a &lt;span class=&#34;caps&#34;&gt;HTML &lt;/span&gt;overview page&lt;/li&gt;
           43 &lt;li&gt;generate a &lt;span class=&#34;caps&#34;&gt;HTML &lt;/span&gt;detail page for each image, linked to the next/prev image&lt;/li&gt;
           44 &lt;li&gt;generate a zip archive of the images&lt;/li&gt;
           45 &lt;li&gt;&lt;span class=&#34;caps&#34;&gt;EXIF &lt;/span&gt;rotation hints are used to properly orient the previews&lt;/li&gt;
           46 &lt;li&gt;images are sorted by the &lt;span class=&#34;caps&#34;&gt;EXIF &lt;/span&gt;timestamp (per default, you could also use mtime)&lt;/li&gt;
           47 &lt;/ul&gt;
           48 
           49 &lt;pre&gt;&lt;code&gt;ls -la /var/web/my-server-net/gallery/2025-10-some-pictures
           50 -rw-rw-r-- 1 domm domm      1370 Sep 26 21:15 20250914_p1370198.html
           51 -rw-rw-r-- 1 domm domm      1370 Sep 26 21:15 20250914_p1370208.html
           52 -rw-rw-r-- 1 domm domm      1370 Sep 26 21:15 20250915_p1370253.html
           53 -rw-rw-r-- 1 domm domm 171738640 Sep 26 21:12 2025-10-some-pictures.zip
           54 -rw-rw-r-- 1 domm domm      3977 Sep 26 21:15 index.html
           55 -rw-rw-r-- 1 domm domm   7322112 Sep 26 21:12 orig_20250914_P1370198.JPG
           56 -rw-rw-r-- 1 domm domm   7354880 Sep 26 21:12 orig_20250914_P1370208.JPG
           57 -rw-rw-r-- 1 domm domm   7686656 Sep 26 21:12 orig_20250915_P1370253.JPG
           58 -rw-rw-r-- 1 domm domm    106382 Sep 26 21:12 preview_20250914_P1370198.JPG
           59 -rw-rw-r-- 1 domm domm    170346 Sep 26 21:12 preview_20250914_P1370208.JPG
           60 -rw-rw-r-- 1 domm domm    133342 Sep 26 21:12 preview_20250915_P1370253.JPG
           61 -rw-rw-r-- 1 domm domm      1434 Sep 26 21:15 snig.css
           62 -rw-rw-r-- 1 domm domm      5128 Sep 26 21:12 thumbnail_20250914_P1370198.JPG
           63 -rw-rw-r-- 1 domm domm      9495 Sep 26 21:12 thumbnail_20250914_P1370208.JPG
           64 -rw-rw-r-- 1 domm domm      6408 Sep 26 21:12 thumbnail_20250915_P1370253.JPG&lt;/code&gt;&lt;/pre&gt;
           65 
           66 &lt;p&gt;You can then take the output folder and rsync it onto a static file server. Or install &lt;code&gt;snig&lt;/code&gt; on your web server and do the conversion there..&lt;/p&gt;
           67 
           68 &lt;p&gt;Again, take a look at the &lt;a href=&#34;https://snig.plix.at/pub/202509_a_few_days_in_tyrol/&#34;&gt;example gallery&lt;/a&gt;.&lt;/p&gt;
           69 
           70 &lt;h3&gt;Limitations&lt;/h3&gt;
           71 
           72 &lt;p&gt;For now, &lt;code&gt;snig&lt;/code&gt; is very simple and stupid, so quite a few things are still hard coded (like copyright and license). If somebody else wants to use it, I will add some more command line flags and/or a config file. But for now this is not needed, so I did not add it.&lt;/p&gt;
           73 
           74 &lt;h3&gt;Have fun&lt;/h3&gt;
           75 
           76 &lt;p&gt;I had quite some fun hacking &lt;code&gt;snig&lt;/code&gt; and also used it as an opportunity to learn the new &lt;code&gt;class&lt;/code&gt; syntax (and fresh up on subroutine / method signatures). So not only did I use my brain, I actually learned something new!&lt;/p&gt;
           77 
           78 &lt;p&gt;Please feel free to give &lt;code&gt;snig&lt;/code&gt; a try. Or just use one of the countless other static image gallery generators. Or just write your own!&lt;/p&gt;
           79 
           80 &lt;p&gt;&lt;a href=&#34;https://lobste.rs/s/bu1a84/i_brain_coded_static_image_gallery_few&#34;&gt;lobste.rs&lt;/a&gt;, &lt;a href=&#34;https://news.ycombinator.com/item?id=45492242&#34;&gt;hacker news&lt;/a&gt;, &lt;a href=&#34;https://www.reddit.com/r/perl/comments/1nzls1e/i_brain_coded_a_static_image_gallery_in_a_few/&#34;&gt;reddit&lt;/a&gt;&lt;/p&gt;
           81 
           82 &lt;h3&gt;Footnotes&lt;/h3&gt;
           83 
           84 &lt;p class=&#34;footnote&#34; id=&#34;fn0&#34;&gt;&lt;sup&gt;0&lt;/sup&gt; As opposed to &lt;a href=&#34;https://en.wikipedia.org/wiki/Vibe_coding&#34;&gt;vibe coding&lt;/a&gt;.&lt;/p&gt;</content><category term="Perl"/><category term="CPAN"/><category term="fun"/><category term="release"/><category term="open source"/><category term="~/bin"/></entry><entry><title>Bike trip from Schwaz to Wels</title><link href="https://domm.plix.at/reisen/2025_biketrip_schwaz_wels.html"/><id>https://domm.plix.at/reisen/2025_biketrip_schwaz_wels.html</id><updated>2025-09-27T15:46:00+02:00</updated><category term="reisen"/><summary type="html">After spending 1.5 weeks in Schwaz with the family looking after a friends cats and house, I cycled back (not the complete distance to Vienna..)
           85 
           86 Day 1: Schwaz - Waidring
           87 
           88 96.75km, 5:40, &#8960; 17.03, ...</summary><content type="html">&lt;p&gt;After spending 1.5 weeks in Schwaz with the family looking after a friends cats and house, I cycled back (not the complete distance to Vienna..)&lt;/p&gt;
           89 
           90 &lt;h3&gt;Day 1: Schwaz - Waidring&lt;/h3&gt;
           91 
           92 &lt;p&gt;96.75km, 5:40, &#8960; 17.03, from 11:00 - 17:30&lt;/p&gt;
           93 
           94 &lt;p&gt;Along the Inn to W&#246;rgel, then into the &#34;Wilder Kaiser&#34; area, very touristy. I once had to stop a bit because a movie shoot (well, actually for some &lt;span class=&#34;caps&#34;&gt;ORF&lt;/span&gt; Krimi) needed a clean view of the small mountain road I was climbing. Stayed in a nice &lt;span class=&#34;caps&#34;&gt;B&#38;amp;B &lt;/span&gt;and had Blunzn for dinner.&lt;/p&gt;
           95 
           96 &lt;h3&gt;Day 2: Waidring - Nu&#223;dorf / Attersse&lt;/h3&gt;
           97 
           98 &lt;p&gt;112km, 5:56, &#8960; 18.86, from 9:45 to 17:30&lt;/p&gt;
           99 
          100 &lt;p&gt;A bit more downhill today, through the very nice area around Lofer and Bad Reichenhall. Got some &#34;fake&#34; Mozartkugeln at a Reber factory shop right before the &#34;border&#34; back into Austria. Drachenwand impressive as always, as is riding through the bike tunnel along Mondsee. Did extra 10km because I could not find a place to sleep. All the &lt;span class=&#34;caps&#34;&gt;B&#38;amp;B&lt;/span&gt;s were &#34;full&#34;, but I think they just did not want to prepare a single room for one night...&lt;/p&gt;
          101 
          102 &lt;h3&gt;Attersse - Wels&lt;/h3&gt;
          103 
          104 &lt;p&gt;72.74km, 3:44, &#8960; 19,41, from 10:00 to 14:15&lt;/p&gt;
          105 
          106 &lt;p&gt;Mostly flat, except some detours along Attersee so the &#34;slow&#34; bikes don&#39;t block the main road running along the shore (I skipped a few, esp the one to &#34;Berg im Attergau&#34;...). Not so nice landscape between Attersee an Schwanstadt (lots of industry), but then some nice parts along Ager and Traun and through small villages. For some weird reason all three Kebab shops &lt;b&gt;and&lt;/b&gt; the big Spar Supermarket next to Wels Railway Station are closed on Saturday, but I found some &#34;Chinese&#34; noodles.&lt;/p&gt;
          107 
          108 &lt;h3&gt;Now sitting in the train back to Vienna.&lt;/h3&gt;
          109 
          110 &lt;p&gt;A nice trip, though I feel my ass and my left knee.&lt;/p&gt;
          111 
          112 &lt;p&gt;I think the next time I do some multi-day long distance cycling I will target ~80km / 4:30h stages, so I can spend some time hacking at fun projects (or work..) for a few hours after cycling.&lt;/p&gt;
          113 
          114 &lt;p&gt;I had my &lt;span class=&#34;caps&#34;&gt;GPS &lt;/span&gt;tracker set to 15sec interval, which proved to be quite inaccurate. For example for day 2 the &lt;span class=&#34;caps&#34;&gt;GPS &lt;/span&gt;tracker recorded 104km, but my old-school simple wired bike computer 112km. Next time I&#39;ll either not record at all, or use a shorter interval (and hope that this won&#39;t kill the battery).&lt;/p&gt;
          115 
          116 &lt;p&gt;Much of this route was actually the same as &lt;a href=&#34;/reisen/2016_rad_wien_innsbruck.html&#34;&gt;this route&lt;/a&gt;, only in the other direction...&lt;/p&gt;</content><category term="bicycle"/></entry><entry><title>Cat and house sitting in Schwaz</title><link href="https://domm.plix.at/reisen/2025_schwaz.html"/><id>https://domm.plix.at/reisen/2025_schwaz.html</id><updated>2025-09-13T12:00:00+02:00</updated><category term="reisen"/><summary>A friend of a friend was doing a lot of long bike trips this year and was looking for people to sit their cats and house. We happily volunteered! We (me, BaHo, Older and Younger Son plus partner) ...</summary><content type="html">&lt;p&gt;A friend of a friend was doing a lot of long bike trips this year and was looking for people to sit their cats and house. We happily volunteered!&lt;/p&gt;
          117 
          118 &lt;p&gt;We (me, BaHo, Older and Younger Son plus partner) spend a nice week there, with some hiking (Kellerjoch, Wolfsklamm, Achensee) and even more relaxing. And tending to the cats...&lt;/p&gt;
          119 
          120 &lt;p&gt;&lt;a href=&#34;https://snig.plix.at/pub/202509_a_few_days_in_tyrol/&#34;&gt;Here&lt;/a&gt; are some more pictures.&lt;/p&gt;</content></entry><entry><title>Installing DarkPAN Perl modules via GitLab</title><link href="https://domm.plix.at/perl/2025_09_install_darkpan_gitlab.html"/><id>https://domm.plix.at/perl/2025_09_install_darkpan_gitlab.html</id><updated>2025-09-07T15:00:00+00:00</updated><category term="perl"/><summary>This week Farhad and me finally found some time to improve a part of our build pipeline that was nagging me for years. We can now release our DarkPAN modules via CI/CD into a GitLab generic packages ...</summary><content type="html">&lt;p&gt;This week &lt;a href=&#34;https://farhad.shahbazi.at/&#34;&gt;Farhad&lt;/a&gt; and me finally found some time to improve a part of our build pipeline that was nagging me for years. We can now release our DarkPAN modules via CI/CD into a &lt;a href=&#34;https://docs.gitlab.com/user/packages/generic_packages/&#34;&gt;GitLab generic packages repository&lt;/a&gt; and install them from there into our app containers, also via CI/CD pipelines.&lt;/p&gt;
          121 
          122 &lt;p&gt;But before we start with the details, a little background:&lt;/p&gt;
          123 
          124 &lt;h3&gt;DarkPAN&lt;/h3&gt;
          125 
          126 &lt;p&gt;&lt;a href=&#34;https://www.perl.com/&#34;&gt;Perl&lt;/a&gt; modules are published to &lt;a href=&#34;https://metacpan.org/&#34;&gt;&lt;span class=&#34;caps&#34;&gt;CPAN&lt;/span&gt;&lt;/a&gt;. But not all Perl code goes to &lt;span class=&#34;caps&#34;&gt;CPAN, &lt;/span&gt;and this &#34;dark Perl matter&#34; is called &#34;DarkPAN&#34;. You especially don&#39;t want to publish the internal code that&#39;s running your company, or the set of various helper code that you collect over the years that&#39;s not really tied to one app, and also (for whatever reasons) not suitable to be properly release to &lt;span class=&#34;caps&#34;&gt;CPAN.&lt;/span&gt; If you still want to use all the best practices and tools established in the last 30+ years, but in private, you can set up an internal &lt;span class=&#34;caps&#34;&gt;CPAN&lt;/span&gt;-like repository, for example using tools like &lt;a href=&#34;https://metacpan.org/dist/Pinto/view/bin/pinto&#34;&gt;Pinto&lt;/a&gt;. Then you can use standard tools to install your internal dependencies from your internal &lt;span class=&#34;caps&#34;&gt;CPAN&lt;/span&gt;-like repo (which I also call DarkPAN).&lt;/p&gt;
          127 
          128 &lt;p&gt;So this is what we did in the last ~10 years:&lt;/p&gt;
          129 
          130 &lt;ul&gt;
          131 &lt;li&gt;We have a bunch of applications&lt;/li&gt;
          132 &lt;li&gt;We have a bunch of libraries that are used by those applications&lt;/li&gt;
          133 &lt;li&gt;When we push a library repo, GitLab will run a CI job that&lt;br /&gt;
          134   * uses &lt;a href=&#34;http://Dist::Zilla&#34;&gt;Dist::Zilla&lt;/a&gt; to build a proper package for the library&lt;br /&gt;
          135   * also uses Dist::Zilla to release the package to our custom Pinto&lt;/li&gt;
          136 &lt;li&gt;When an app needs to use such a library (or a new version), we add it to the apps &lt;code&gt;cpanfile&lt;/code&gt;&lt;/li&gt;
          137 &lt;li&gt;And when a new release of the app is built (again via GitLab CI/CD pipeline), we use &lt;a href=&#34;https://metacpan.org/dist/App-cpm/view/script/cpm&#34;&gt;cpm&lt;/a&gt; with a custom &lt;code&gt;resolver&lt;/code&gt; to install the dependency from our DarkPAN&lt;/li&gt;
          138 &lt;/ul&gt;
          139 
          140 &lt;p&gt;&lt;code&gt;cpm install -g --resolver metadb --resolver 02packages,https://pinto.internal.example.com/stacks/darkpan&lt;/code&gt;&lt;/p&gt;
          141 
          142 &lt;p&gt;This worked quite well.&lt;/p&gt;
          143 
          144 &lt;h3&gt;Shared Libs&lt;/h3&gt;
          145 
          146 &lt;p&gt;It worked so well that we also used this to release and install what I now call &#34;shared libs&#34; inside monorepos: We have a few monorepos for different projects, where each monorepo contains multiple apps (different &lt;span class=&#34;caps&#34;&gt;API &lt;/span&gt;backends, frontends and anything in between). Inside a monorepo we have code that we want to share between all apps, for example the database abstraction (DBIx::Class). Deploying these shared libs via the above method was working, but not very smoothly, especially when there&#39;s a lot of development happening: You had to push a new version of the lib, wait for it to be released, and then bump the version in all the &lt;code&gt;cpanfiles&lt;/code&gt; using this library.&lt;/p&gt;
          147 
          148 &lt;p&gt;So a few weeks ago I changed our handling of these shared libs. Instead of properly installing them via &lt;code&gt;cpm&lt;/code&gt;, I can copy the source code directly into the app container and set &lt;code&gt;PERL5LIB&lt;/code&gt; accordingly. This is possible because all of the code is in the same monorepo and thus available in the CI/CD pipeline. (material for another blog post..)&lt;/p&gt;
          149 
          150 &lt;p&gt;This hack is not an (easy) option for code that has to be shared between projects. But I wanted to get rid of maintaining a server to host Pinto, especially as we already have GitLab running, which supports a large range of &lt;a href=&#34;https://docs.gitlab.com/user/packages/package_registry/&#34;&gt;language specific repositories&lt;/a&gt;. Unfortunately, Perl/CPAN is not implemented. But they have a &#34;generic&#34; repository, so I tried to use it to solve our problem.&lt;/p&gt;
          151 
          152 &lt;h3&gt;Publishing a Perl package to into a GitLab generic packages repository&lt;/h3&gt;
          153 
          154 &lt;p&gt;The first step is to publish the freshly build Perl distribution into the GitLab repo. This is easy to do via the GitLab &lt;span class=&#34;caps&#34;&gt;API &lt;/span&gt;and &lt;code&gt;curl&lt;/code&gt;. The &lt;span class=&#34;caps&#34;&gt;API &lt;/span&gt;endpoint is not very nice &lt;span class=&#34;caps&#34;&gt;IMO&lt;/span&gt;: &lt;code&gt;api/v4/projects/{project-id}/packages/generic/{name}/{version}/{file}&lt;/code&gt;, and I find it a bit weird that you set up a &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;version&lt;/code&gt; and then add files to it (instead of just uploading a tarball), but whatever.&lt;/p&gt;
          155 
          156 &lt;p&gt;In our &lt;code&gt;Makefile&lt;/code&gt; we added:&lt;/p&gt;
          157 
          158 &lt;pre&gt;&lt;code&gt;package_registry := &#38;quot;https://gitlab.example.com/api/v4/projects/foo%2Fbar/packages/generic/$(application_name)&#38;quot;
          159 get_version = $(shell ls $(application_name)-*.tar.gz | sed &#38;quot;s/$(application_name)-//; s/\.tar\.gz//&#38;quot;)
          160 get_filename = $(shell ls $(application_name)-*.tar.gz)
          161 private_token ?= ${CI_JOB_TOKEN}
          162 
          163 .PHONY: build
          164 build:
          165         dzil authordeps --missing | xargs -r cpm install --global
          166         cpm install --global
          167         dzil build
          168 
          169 .PHONY: release
          170 release:
          171         $(eval VERSION := $(get_version))
          172         $(eval FILENAME := $(get_filename))
          173         curl -L -H &#38;quot;JOB-TOKEN: $(private_token)&#38;quot; \
          174                 --upload-file &#38;quot;$(FILENAME)&#38;quot; \
          175                 &#38;quot;$(package_registry)/$(VERSION)/$(FILENAME)&#38;quot;&lt;/code&gt;&lt;/pre&gt;
          176 
          177 &lt;p&gt;We set the &lt;code&gt;package_registry&lt;/code&gt; base &lt;span class=&#34;caps&#34;&gt;URL &lt;/span&gt;(note that I use the &lt;span class=&#34;caps&#34;&gt;URI&lt;/span&gt;-escaped string &lt;code&gt;foo%2Fbar&lt;/code&gt; to address the project &lt;code&gt;bar&lt;/code&gt; in group &lt;code&gt;foo&lt;/code&gt;, because I find using the project ID even uglier that escaping the &lt;code&gt;/&lt;/code&gt; as &lt;code&gt;%2F&lt;/code&gt;). Then we use some shell / sed to &#34;parse&#34; the filename and get the version number (which is set automatically via Dist::Zilla) in &lt;code&gt;build&lt;/code&gt;.&lt;/p&gt;
          178 
          179 &lt;p&gt;In &lt;code&gt;release&lt;/code&gt;, we construct the final &lt;span class=&#34;caps&#34;&gt;URL, &lt;/span&gt;authorize using the &lt;code&gt;CI_JOB_TOKEN&lt;/code&gt; and call &lt;code&gt;curl&lt;/code&gt; to upload the file.&lt;/p&gt;
          180 
          181 &lt;h4&gt;Why are we using a &lt;code&gt;Makefile&lt;/code&gt; instead of defining the steps in &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;?&lt;/h4&gt;
          182 
          183 &lt;ul&gt;
          184 &lt;li&gt;I can use the &lt;code&gt;Makefile&lt;/code&gt; locally, so I can run whatever steps that are triggered by the pipeline without having to push a change to gitlab (no more &lt;code&gt;force pipeline&lt;/code&gt; commits!)&lt;/li&gt;
          185 &lt;li&gt;We prefer writing &lt;code&gt;Makefile&lt;/code&gt; to &#34;programming&#34; in &lt;span class=&#34;caps&#34;&gt;YAML&lt;/span&gt;&lt;/li&gt;
          186 &lt;li&gt;The CI stages are very short: &lt;code&gt;script:  - make build release&lt;/code&gt;&lt;/li&gt;
          187 &lt;/ul&gt;
          188 
          189 &lt;h3&gt;Installing from GitLab&lt;/h3&gt;
          190 
          191 &lt;p&gt;Actually installing the Perl distribution from GitLab seemed easy, but GitLab threw a few stumbling blocks in our way.&lt;/p&gt;
          192 
          193 &lt;p&gt;My plan was to just take the &lt;span class=&#34;caps&#34;&gt;URL &lt;/span&gt;of the distribution in the GitLab Generic Registry and use that in &lt;code&gt;cpanfile&lt;/code&gt; via the nice &lt;code&gt;url&lt;/code&gt; addon which allows you to install a &lt;span class=&#34;caps&#34;&gt;CPAN &lt;/span&gt;distribution directly from the given &lt;span class=&#34;caps&#34;&gt;URL&lt;/span&gt;:&lt;/p&gt;
          194 
          195 &lt;pre&gt;&lt;code&gt;requires &#39;Internal::Package&#39; =&#38;gt; &#39;1.42&#39;, url =&#38;gt; &#39;https://gitlab.example.com/foo/bar/packages/generic/Internal-Package-1.42.tar.gz&#39;;&lt;/code&gt;&lt;/pre&gt;
          196 
          197 &lt;p&gt;But for weird reasons, GitLab does not provide a simple &lt;span class=&#34;caps&#34;&gt;URL &lt;/span&gt;like that, esp not for unauthorized users. Instead you have to call the &lt;span class=&#34;caps&#34;&gt;API, &lt;/span&gt;provide a token and only then you can download the tarball. And there is no way to add a custom header to pass the token in &lt;code&gt;cpanfile&lt;/code&gt;.&lt;/p&gt;
          198 
          199 &lt;p&gt;After some more reading of the docs, we found that instead of using a header, we can also stuff a deploy token into basic auth using &lt;code&gt;https://user:password@url&lt;/code&gt; format, and thus can specify the install &lt;span class=&#34;caps&#34;&gt;URL &lt;/span&gt;like this:&lt;/p&gt;
          200 
          201 &lt;pre&gt;&lt;code&gt;requires &#39;Internal::Package&#39; =&#38;gt; &#39;1.42&#39;, url =&#38;gt; &#39;https://deployer:gldt-234lkndfg@gitlab.example.com/foo/bar/packages/generic/Internal-Package-1.42.tar.gz&#39;;&lt;/code&gt;&lt;/pre&gt;
          202 
          203 &lt;p&gt;And this works!!&lt;/p&gt;
          204 
          205 &lt;p&gt;Well, the &lt;span class=&#34;caps&#34;&gt;URL &lt;/span&gt;(using the actual &lt;span class=&#34;caps&#34;&gt;API &lt;/span&gt;call) in fact looks like this:&lt;/p&gt;
          206 
          207 &lt;pre&gt;&lt;code&gt;requires &#39;Internal::Package&#39; =&#38;gt; &#39;1.42&#39;, url =&#38;gt; 
          208 &#39;https://deployer:gldt-234lkndfg@gitlab.example.com/api/v4/projects/foo%2Fbar/packages/generic/Internal-Package/1.42/Internal-Package-1.42.tar.gz/&#39;;&lt;/code&gt;&lt;/pre&gt;
          209 
          210 &lt;p&gt;This is not very nice:&lt;/p&gt;
          211 
          212 &lt;ul&gt;
          213 &lt;li&gt;The token is embedded in the &lt;code&gt;cpanfile&lt;/code&gt;&lt;/li&gt;
          214 &lt;li&gt;We have to define the package name three times&lt;/li&gt;
          215 &lt;li&gt;And we also have to define the version three times, and adapt it correctly every time we release a new version&lt;/li&gt;
          216 &lt;/ul&gt;
          217 
          218 &lt;p&gt;So we continued to improve this in a maybe crazy but perlish way:&lt;/p&gt;
          219 
          220 &lt;h3&gt;Dynamic cpanfile&lt;/h3&gt;
          221 
          222 &lt;p&gt;One of the nice things of &lt;code&gt;cpanfile&lt;/code&gt; (and Perl in general) is that instead of inventing some stupid &lt;span class=&#34;caps&#34;&gt;DSL &lt;/span&gt;to specify your requirements, we just use code:&lt;/p&gt;
          223 
          224 &lt;pre&gt;&lt;code&gt;requires &#39;Foo::Bar&#39;;&lt;/code&gt;&lt;/pre&gt;
          225 
          226 &lt;p&gt;is actually calling a function somewhere that does stuff.&lt;/p&gt;
          227 
          228 &lt;p&gt;So we can run code in &lt;code&gt;cpanfile&lt;/code&gt;:&lt;/p&gt;
          229 
          230 &lt;pre&gt;&lt;code&gt;my @DB = qw(Pg mysql DB2 CSV);
          231 my $yolo_db = &#39;DB::&#39; . $DB[rand @DB];
          232 requires $yolo_db;&lt;/code&gt;&lt;/pre&gt;
          233 
          234 &lt;p&gt;The above code is of course crazy, but we can use this power for good and write a nice little wrapper to make depending on our DarkPAN easier.&lt;/p&gt;
          235 
          236 &lt;h3&gt;Validad::InstallFromGitlab&lt;/h3&gt;
          237 
          238 &lt;p&gt;I wrote a small tool, &lt;code&gt;Validad::InstallFromGitlab&lt;/code&gt;, which is configured with the GitLab base &lt;span class=&#34;caps&#34;&gt;URL, &lt;/span&gt;the project name and the token. It exports a function &lt;code&gt;from_gitlab&lt;/code&gt;, which takes the name and the version of the distribution and returns the long line that &lt;code&gt;requires&lt;/code&gt; needs.&lt;/p&gt;
          239 
          240 &lt;p&gt;And because &lt;code&gt;cpanfile&lt;/code&gt; is just Perl, we can easily use this module there:&lt;/p&gt;
          241 
          242 &lt;pre&gt;&lt;code&gt;use Validad::InstallFromGitlab (gitlab =&#38;gt; &#39;https://gitlab.example.com&#39;, project =&#38;gt; &#39;validad%2fcontainer&#39;, auth =&#38;gt; $ENV{DARKPAN_ACCESS});
          243 requires from_gitlab( &#39;Validad::Mailer&#39; =&#38;gt; &#39;1.20250904.144530&#39;);
          244 requires from_gitlab( &#39;Accounts::Client&#39; =&#38;gt; &#39;1.20250904.144543&#39;);&lt;/code&gt;&lt;/pre&gt;
          245 
          246 &lt;p&gt;I decided to use some rather old but very powerful method to make &lt;code&gt;Validad::InstallFromGitlab&lt;/code&gt; easy to use: A custom &lt;code&gt;import()&lt;/code&gt; function:&lt;/p&gt;
          247 
          248 &lt;pre&gt;&lt;code&gt;package Validad::InstallFromGitlab;
          249 use v5.40;
          250 use Carp qw(croak);
          251 
          252 sub import {
          253     my ($class, %args) = @_;
          254     if (!$args{gitlab} || !$args{project}) {
          255         croak(&#38;quot;gitlab and/or project missing&#38;quot;);
          256     }
          257 
          258     my $registry_url = sprintf(&#39;%s/api/v4/projects/%s/packages/generic&#39;, $args{gitlab}, $args{project});
          259     if (my $auth = $args{auth}) {
          260         $registry_url =~s{://}{&#39;://&#39;.$auth.&#39;@&#39;}e;
          261     }
          262 
          263     my $caller=caller();
          264     no strict &#39;refs&#39;;
          265     *{&#38;quot;$caller\::from_gitlab&#38;quot;} = sub {
          266         my ($module, $version) = @_;
          267 
          268         my $package = $module;
          269         $package =~s/::/-/g;
          270         my $tarball = $package .&#39;-&#39; . $version . &#39;.tar.gz&#39; ;
          271         my $url = join(&#39;/&#39;,$registry_url, $package, $version, $tarball);
          272         return ($module, url =&#38;gt; $url);
          273     };
          274 }
          275 
          276 1;&lt;/code&gt;&lt;/pre&gt;
          277 
          278 &lt;p&gt;&lt;code&gt;import()&lt;/code&gt; is called when you use the module in the calling code or &lt;code&gt;cpanfile&lt;/code&gt; :-)&lt;/p&gt;
          279 
          280 &lt;pre&gt;&lt;code&gt;use Validad::InstallFromGitlab (
          281     gitlab  =&#38;gt; &#39;https://gitlab.example.com&#39;,
          282     project =&#38;gt; &#39;validad%2fcontainer&#39;,
          283     auth    =&#38;gt; $ENV{DARKPAN_ACCESS}
          284 );&lt;/code&gt;&lt;/pre&gt;
          285 
          286 &lt;p&gt;The parameters passed to &lt;code&gt;use&lt;/code&gt; are passed on to &lt;code&gt;import&lt;/code&gt;, where I do some light checking and build the long and cumbersome GitLab url.&lt;/p&gt;
          287 
          288 &lt;p&gt;Then I use &lt;code&gt;caller()&lt;/code&gt; to get the name of the calling namespace and use a typoglob (been some time since I used that..) to install a function named &lt;code&gt;from_gitlab&lt;/code&gt; into the caller.&lt;/p&gt;
          289 
          290 &lt;p&gt;This function takes two params, the module name and version, and finally constructs the data needed by require, i.e. the module name, version and the whole gitlab &lt;span class=&#34;caps&#34;&gt;URL.&lt;/span&gt;&lt;/p&gt;
          291 
          292 &lt;p&gt;So I can now specify my requirements very easily in the apps &lt;code&gt;cpanfiles&lt;/code&gt; and still use the GitLab generic package registry to distribute my DarkPAN modules!&lt;/p&gt;
          293 
          294 &lt;h3&gt;Installing Validad::InstallFromGitlab&lt;/h3&gt;
          295 
          296 &lt;p&gt;But how do I install &lt;code&gt;Validad::InstallFromGitlab&lt;/code&gt;?&lt;/p&gt;
          297 
          298 &lt;p&gt;I don&#39;t. But all of our apps use a shared base container (which also helps to keep container size down). And in the Containerfile of the base container, I copy &lt;code&gt;Validad::InstallFromGitlab&lt;/code&gt; to a well-known location &lt;code&gt;/opt/perl/darkpan&lt;/code&gt; and load it from there via &lt;code&gt;PERL5LIB&lt;/code&gt;:&lt;/p&gt;
          299 
          300 &lt;pre&gt;&lt;code&gt;RUN mkdir -p /opt/perl/darkpan/Validad/
          301 COPY Validad-InstallFromGitlab.pm /opt/perl/darkpan/Validad/InstallFromGitlab.pm
          302 
          303 ONBUILD ARG DARKPAN_ACCESS
          304 ONBUILD RUN PERL5LIB=/opt/perl/darkpan/ \
          305             /opt/perl/bin/cpm install --cpanfile cpanfile \
          306                                       --show-build-log-on-failure -g&lt;/code&gt;&lt;/pre&gt;
          307 
          308 &lt;p&gt;But again that&#39;s material for another blog post...&lt;/p&gt;</content><category term="Perl"/><category term="gitlab"/><category term="DevOps"/><category term="CI"/><category term="container"/><category term="CPAN"/></entry><entry><title>Koha Hackfest 2025 in Marseille</title><link href="https://domm.plix.at/perl/2025_04_koha_hackfest.html"/><id>https://domm.plix.at/perl/2025_04_koha_hackfest.html</id><updated>2025-04-04T10:00:00+00:00</updated><category term="perl"/><summary type="html">I&#39;m currently sitting in a TGV doing 300km/h from Marseille to Paris, traveling back home from the Koha Hackfest, hosted by BibLibre.
          309 
          310 Results
          311 
          312 This year I did a lot of QA, which means reviewing ...</summary><content type="html">&lt;p&gt;I&#39;m currently sitting in a &lt;span class=&#34;caps&#34;&gt;TGV &lt;/span&gt;doing 300km/h from Marseille to Paris, traveling back home from the &lt;a href=&#34;https://koha-community.org/&#34;&gt;Koha&lt;/a&gt; Hackfest, hosted by &lt;a href=&#34;https://www.biblibre.com/&#34;&gt;BibLibre&lt;/a&gt;.&lt;/p&gt;
          313 
          314 &lt;h3&gt;Results&lt;/h3&gt;
          315 
          316 &lt;p&gt;This year I did a lot of &lt;span class=&#34;caps&#34;&gt;QA, &lt;/span&gt;which means reviewing patches, running their test plan, verifying that everything works and finally signing off the patches and marking the bug as &#34;Passed QA&#34;. The process is documented in &lt;a href=&#34;https://wiki.koha-community.org/wiki/How_to_QA&#34;&gt;the wiki&lt;/a&gt;. According to the &lt;a href=&#34;https://scoreboard.koha-community.org/&#34;&gt;scoreboard&lt;/a&gt; I &lt;span class=&#34;caps&#34;&gt;QA&#39;&lt;/span&gt;ed 8 bugs (the second highest number!). After the third or fourth time I did not even have to look up all the steps anymore.&lt;/p&gt;
          317 
          318 &lt;p&gt;I moderated a short panel on ElasticSearch, because I found some weird behaviors on which I needed feedback from the experts. This resulted in a bunch of new &#34;bugs&#34; (Koha speak for issues, in this case a mix of actual bugs an feature requests): &lt;a href=&#34;https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39493&#34;&gt;39494&lt;/a&gt;, &lt;a href=&#34;https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39548&#34;&gt;39548&lt;/a&gt;, &lt;a href=&#34;https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39549&#34;&gt;39549&lt;/a&gt;, &lt;a href=&#34;https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39551&#34;&gt;39551&lt;/a&gt;, &lt;a href=&#34;https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=39552&#34;&gt;39552&lt;/a&gt;.&lt;/p&gt;
          319 
          320 &lt;p&gt;I did a rather detailed review of &lt;a href=&#34;https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=37020&#34;&gt;37020 - bulkmarcimport gets killed when inserting large files&lt;/a&gt;. The problem here is that the current code uses &lt;span class=&#34;caps&#34;&gt;MARC&lt;/span&gt;::Batch, which does some horrible regex &#34;parsing&#34; of &lt;span class=&#34;caps&#34;&gt;XML &lt;/span&gt;to implement a stream parser (so it can handle large files without using &lt;span class=&#34;caps&#34;&gt;ALL &lt;/span&gt;the &lt;span class=&#34;caps&#34;&gt;RAM&lt;/span&gt;) (see more details at the end of this post). But a &lt;a href=&#34;https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=37478&#34;&gt;recent change&lt;/a&gt; added a check-step which validates the records and puts the valid ones onto an Perl array. Which now again takes up &lt;span class=&#34;caps&#34;&gt;ALL &lt;/span&gt;the &lt;span class=&#34;caps&#34;&gt;RAM.&lt;/span&gt; I reviewed the two proposed patches, but I think we should use &lt;span class=&#34;caps&#34;&gt;XML&lt;/span&gt;::LibXML::Reader directly, which should result in cleaner, faster, less-RAM-using and correct code.&lt;/p&gt;
          321 
          322 &lt;p&gt;I also participated in various other discussions and hope to have provided some helpful ideas &#38;amp; feedback from my still semi-external Koha perspective and semi-extensive knowledge of other environments and projects (I have been doing this &#34;web dev&#34; stuff for quite some time now..).&lt;/p&gt;
          323 
          324 &lt;p&gt;After help Clemens setup &lt;span class=&#34;caps&#34;&gt;L10N &lt;/span&gt;on his &lt;span class=&#34;caps&#34;&gt;KTD &lt;/span&gt;setup, I submitted a &lt;a href=&#34;https://gitlab.com/koha-community/koha-testing-docker/-/merge_requests/540&#34;&gt;doc patch&lt;/a&gt; to &lt;span class=&#34;caps&#34;&gt;KTD &lt;/span&gt;explaining the &lt;code&gt;SKIP_L10N&lt;/code&gt; setup and hopefully making the general &lt;span class=&#34;caps&#34;&gt;L10N &lt;/span&gt;setup a bit clearer. I generally try to improve the docs if I hit a problem and was able to fix it. Give it a try the next time, it&#39;s very rewarding!&lt;/p&gt;
          325 
          326 &lt;p&gt;I could also provide some Perl help to various other attendees. But I still failed most of the questions of joubus &lt;a href=&#34;https://2025-hackfest-perl-quizz-e8dbca.gitlab.io/perl_quizz.html&#34;&gt;Perl quiz&lt;/a&gt;. My excuse is that I trained my brain on writing only good/sane/nice Perl so that I forgot how to parse all the &lt;a href=&#34;https://domm.plix.at/perl/space_invaders.pl.txt&#34;&gt;weird corner cases&lt;/a&gt;...&lt;/p&gt;
          327 
          328 &lt;h3&gt;Social&lt;/h3&gt;
          329 
          330 &lt;p&gt;But the Hackfest is not only about hacking, there&#39;s also the &#34;fest&#34; part (or party?). I really enjoyed hanging out with the other attendees on the terrace during lunch in the sun. The food was as usual excellent and not too unhealthy (of course depending on how much cheese one is able to stack onto his plate). The evenings at various bars and restaurants where fun and entertaining (even though I did manage to go to bed early enough this year, and hardly had any alcohol).&lt;/p&gt;
          331 
          332 &lt;p&gt;I did not do any sightseeing or even just walking around Marseille this year. I blame the fact that our hotel was very near to the venue and most of the after-hack locations. And I didn&#39;t bring my swimming trunks so I was not motivated to go to the beach (but I&#39;ve ticked that off last year..)&lt;/p&gt;
          333 
          334 &lt;p&gt;I had a lot of nice chats with old and new friends on topics ranging from the obvious (A.I., the sorry state of the world, Koha, Perl) to the obscure (US garbage collection trucks, the lifetime of ropes for hand-pulled elevators up to Greek monasteries, sweet potato heritage of Aotearoa, chicken egg sizes, anarcho-syndicalism, ...)&lt;/p&gt;
          335 
          336 &lt;h3&gt;Thanks&lt;/h3&gt;
          337 
          338 &lt;p&gt;Thanks to BibLibre and Paul Poulain for organizing the event, and to all the attendees for making it such a wonderful 4 days!&lt;/p&gt;
          339 
          340 
          341 &lt;h4&gt;Postscript: The horrors of &lt;span class=&#34;caps&#34;&gt;MARC&lt;/span&gt;::Batch&lt;/h4&gt;
          342 
          343 &lt;p&gt;So, how does &lt;a href=&#34;https://metacpan.org/pod/MARC::Batch&#34;&gt;&lt;span class=&#34;caps&#34;&gt;MARC&lt;/span&gt;::Batch&lt;/a&gt; handle importing huge &lt;span class=&#34;caps&#34;&gt;XML &lt;/span&gt;files without using too much &lt;span class=&#34;caps&#34;&gt;RAM&lt;/span&gt;?&lt;/p&gt;
          344 
          345 &lt;p&gt;By breaking the first rule of &lt;span class=&#34;caps&#34;&gt;XML &lt;/span&gt;handling: It &#34;parses&#34; the &lt;span class=&#34;caps&#34;&gt;XML &lt;/span&gt;via regex!&lt;/p&gt;
          346 
          347 &lt;p&gt;This is actually implemented in &lt;a href=&#34;https://metacpan.org/pod/MARC::File::XML&#34;&gt;&lt;span class=&#34;caps&#34;&gt;MARC&lt;/span&gt;::File::XML&lt;/a&gt;, namely &lt;a href=&#34;https://metacpan.org/dist/MARC-File-XML/source/lib/MARC/File/XML.pm#L385&#34;&gt;here&lt;/a&gt;. If you have a strong stomach I&#39;ll wait for you to take a look at that code.&lt;/p&gt;
          348 
          349 &lt;p&gt;Here are some &#34;highlights&#34;:&lt;/p&gt;
          350 
          351 &lt;pre&gt;&lt;code&gt;    ## get a chunk of xml for a record
          352     local $/ = &#39;record&#38;gt;&#39;;
          353     my $xml = &#38;lt;$fh&#38;gt;;&lt;/code&gt;&lt;/pre&gt;
          354 
          355 &lt;p&gt;Set the &lt;code&gt;input record separator&lt;/code&gt; (usually newline &lt;code&gt;\n&lt;/code&gt;, and telling Perl what it should consider a line) to the string &lt;code&gt;record&#38;gt;&lt;/code&gt; so, basically something which looks like and &lt;span class=&#34;caps&#34;&gt;XML &lt;/span&gt;tag ending with record. It is &lt;span class=&#34;caps&#34;&gt;NOT &lt;/span&gt;including the start &lt;code&gt;&#38;lt;&lt;/code&gt; because the code wants to ignore &lt;span class=&#34;caps&#34;&gt;XML &lt;/span&gt;namespaces.&lt;/p&gt;
          356 
          357 &lt;p&gt;The it uses &lt;code&gt;&#38;lt;$fh&#38;gt;&lt;/code&gt; to read the next &#34;line&#34; from the record, which isn&#39;t a line in any usual sense, but all bytes up to the next occurrence of &lt;code&gt;record&#38;gt;&lt;/code&gt;.&lt;/p&gt;
          358 
          359 &lt;pre&gt;&lt;code&gt;    ## do we have enough?
          360     $xml .= &#38;lt;$fh&#38;gt; if $xml !~ m!&#38;lt;/([^:]+:){0,1}record&#38;gt;$!;&lt;/code&gt;&lt;/pre&gt;
          361 
          362 &lt;p&gt;It continues reading until it find something that looks like a closing &lt;code&gt;&#38;lt;/record&#38;gt;&lt;/code&gt; tag (which might contain a namespace). Then some more &#34;cleanup&#34;, and finally the xml chunk is returned.&lt;/p&gt;
          363 
          364 &lt;p&gt;Obviously this works, as it is used by thousands of libraries around the world on millions of records all the time.&lt;/p&gt;
          365 
          366