Table of contents
I wrote a few Perl scripts for various things during the last few years. Many of them run on machines which I don't maintain on my own (other people have root access, too) and some even run on machines which I don't have access to at all. All of the former machines run Debian, so this article only cares about Debian.
Reasons for packaging
The naive approach to distributing any kind of scripts is to just upload the script somewhere. Or directly put it on some machine. Or email it to someone. And for testing if your program works, that’s great. But after a short amount of time, especially if other people are involved, you should consider packaging your script properly:
-
If your script is running permanently, and you are not around when the
machine it is running on crashes or needs to be restarted, others have a hard
time figuring out how to restore whichever service it was providing (if they
care at all). This reduces the dependencies on you and it reduces your
cognitive load.
Strictly speaking, the benefit here is due to having an initscript (or similar), but I consider that part of a proper package, which is why I’m listing this.
- All dependencies for your script are properly specified, so that the initial deployment is easy (you don’t have to use CPAN at all, assuming all dependencies are available as Debian packages). More importantly, future upgrades become easier. Debian will automatically take care of upgrading your dependencies and making sure they are around. Without having a package, nobody will know that you need these dependencies. Also, nobody knows your code is on the system in case anything breaks on the next upgrade.
-
Having a proper package leads to just having to use
dpkg -l packagename
to figure out which version you are running on this machine. Also, to find all files which are relevant to your script (think modules it installs, configuration files, …), you can usedpkg -L packagename
. - Following the best practise of building the package out of a fresh checkout of your SCM repository and only ever installing packages on the target machine will force you to have all files under version control and no files lying around on your particular computer which are necessary to run the script.
Most of these reasons might not be an immediate benefit for you. But they will make your life so much easier in the future :).
File layout for your script
Often, Perl scripts start as "foo.pl" or similar. To make creating a Makefile.PL and the Debian package easier, you should consider the directory following layout:
/ /lib /lib/Myprogram /lib/Myprogram/functionality.pm (module which implements your stuff) /script /script/myprogram (executable script) /Makefile.PL
As you can see, it’s not complex, but you should place your core functionality in a module within lib/ and have a little wrapper script (/script/myprogram) which just uses that module. This is good style because dh-make-perl, a tool to help us with the Debian packaging, will then find the dependencies which your code has and automatically add them to the debian/control file.
In the wrapper script, you can handle things such as providing a useful description and synopsis when called without arguments and generally handling commandline-arguments. It is good style to not use the .pl extension because users generally don’t care in which language a tool is written in. Should you ever change the language, you would need to change the name.
Makefile.PL
In the Perl world, Makefile.PL is equivalent to the various kinds of Makefiles that other languages use. We have the advantage of being able to depend on Perl, though, which leads to much simpler, cleaner and nicer files :).
In general, you use some Perl module to help you write a Makefile (without .PL)
when the user calls perl Makefile.PL
. A good module for that is
Module::Install.
A typical Makefile.PL looks like this:
#!/usr/bin/env perl use strict; use warnings; use inc::Module::Install; name 'RaumZeitPinSync'; all_from 'lib/RaumZeitLabor/BenutzerDB/Pinsync.pm'; requires 'AnyEvent'; requires 'AnyEvent::HTTP'; requires 'JSON::XS'; install_script 'pin-sync'; WriteAll;
One gotcha here is the name of your distribution. You should not use dashes (-) in it, since they have a special meaning (they replace ::, so the AnyEvent::I3 module is in the AnyEvent-I3 distribution).
Using make install
after generating the Makefile will install
script/pin-sync to /usr/bin and lib/RaumZeitLabor/BenutzerDB/Pinsync.pm to
/usr/share/perl5.
POD
To automatically make the Makefile and dh-make-perl pick up your author information, version number and description, add a block like the following to your module (the one you specify using the all_from keyword, in our case lib/RaumZeitLabor/BenutzerDB/Pinsync.pm):
=head1 NAME RaumZeitPinSync - Syncs PINs to the Pinpad controller EEPROM =head1 DESCRIPTION This module synchronizes our user-specific PINs to the Pinpad controller EEPROM. =head1 VERSION Version 1.0 =head1 AUTHOR Michael Stapelberg, C<< <michael at stapelberg.de> >> =head1 LICENSE AND COPYRIGHT Copyright 2011 Michael Stapelberg. This program is free software; you can redistribute it and/or modify it under the terms of the BSD license. =cut
Debian packaging
We’re far enough to actually generate Debian packaging now. We are going to use dh-make-perl to generate a template for that (often, the template is good enough to require no further changes). It is normally used to generate Debian packages from CPAN modules, so we have to override the default package name. Furthermore, we use source format 1 which does not require an orig-tarball being around in the parent directory:
perl Makefile.PL dh-make-perl -p raumzeitpinsync --source-format 1
Now you have a few files in the debian/ directory. Ignore these for now and see what building a package gets us:
dpkg-buildpackage dpkg -c ../raumzeitpinsync*.deb dpkg -I ../raumzeitpinsync*.deb
You should get an output like this:
... drwxr-xr-x root/root ./usr/share/perl5/ drwxr-xr-x root/root ./usr/share/perl5/RaumZeitLabor/ drwxr-xr-x root/root ./usr/share/perl5/RaumZeitLabor/BenutzerDB/ -rw-r--r-- root/root ./usr/share/perl5/RaumZeitLabor/BenutzerDB/Pinsync.pm drwxr-xr-x root/root ./usr/share/man/ drwxr-xr-x root/root ./usr/share/man/man3/ -rw-r--r-- root/root ./usr/share/man/man3/RaumZeitLabor::BenutzerDB::Pinsync.3pm.gz drwxr-xr-x root/root ./usr/bin/ -rwxr-xr-x root/root ./usr/bin/pin-sync ... Package: raumzeitpinsync Version: 1.0-1 Architecture: all Maintainer: Michael Stapelberg <michael stapelberg.de> Installed-Size: 15 Depends: perl, libanyevent-http-perl, libanyevent-perl, libjson-xs-perl Section: perl Priority: optional Homepage: http://search.cpan.org/dist/RaumZeitPinSync/ Description: Syncs PINs to the Pinpad controller EEPROM (no description was found) . This description was automagically extracted from the module by dh-make-perl.
Additional files (initscript, logrotate config, …)
Often, a script needs additional files to run. For example an initscript to start on system boot. Or a webserver configuration, a config file, a logrotate configuration, etc…
To easily install additional files, we can just modify our Makefile.PL to look like this:
#!/usr/bin/env perl use strict; use warnings; use inc::Module::Install; name 'RaumZeitPinSync'; all_from 'lib/RaumZeitLabor/BenutzerDB/Pinsync.pm'; requires 'AnyEvent'; requires 'AnyEvent::HTTP'; requires 'JSON::XS'; install_script 'pin-sync'; postamble <<'END_OF_MAKEFILE'; install:: extra_install pure_install:: extra_install install_vendor:: extra_install extra_install: install -d $(DESTDIR)/etc/ install -m 640 pin-sync.yml.ex $(DESTDIR)/etc/pin-sync.yml END_OF_MAKEFILE WriteAll;
For an initscript, you can copy /usr/share/debhelper/dh_make/debian/init.d.ex to debian/raumzeitpinsync.init (the filename is relevant) and modify it accordingly.
Afterwards, re-run perl Makefile.PL and dpkg-buildpackage. Enjoy your package!
I run a blog since 2005, spreading knowledge and experience for almost 20 years! :)
If you want to support my work, you can buy me a coffee.
Thank you for your support! ❤️