├── .bashrc ├── .dockerignore ├── .github └── workflows │ └── publish-image.yml ├── .gitignore ├── .vimrc ├── Debian ├── Dockerfile ├── bin │ ├── cyd │ └── lib │ │ └── Cyrus │ │ ├── Docker.pm │ │ └── Docker │ │ └── Command │ │ ├── build.pm │ │ ├── clean.pm │ │ ├── clone.pm │ │ ├── idle.pm │ │ ├── makedocs.pm │ │ ├── shell.pm │ │ ├── smoke.pm │ │ └── test.pm └── dot.bashrc ├── README.md ├── bin └── dar └── fatpacked ├── .gitignore ├── Makefile └── dar /.bashrc: -------------------------------------------------------------------------------- 1 | # .bashrc 2 | 3 | # Source global definitions 4 | if [ -f /etc/bashrc ]; then 5 | . /etc/bashrc 6 | fi 7 | 8 | if [ -f /etc/bash_completion ]; then 9 | . /etc/bash_completion 10 | fi 11 | 12 | alias ls='ls $LS_OPTIONS' 13 | alias ll='ls $LS_OPTIONS -l' 14 | alias l='ls $LS_OPTIONS -lA' 15 | 16 | # Some more alias to avoid making mistakes: 17 | alias rm='rm -i' 18 | alias cp='cp -i' 19 | alias mv='mv -i' 20 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *.log 3 | Makefile 4 | -------------------------------------------------------------------------------- /.github/workflows/publish-image.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | push: 5 | branches: [ 'dev', 'master' ] 6 | schedule: 7 | - cron: '0 3 * * *' 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build-and-push-image: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | platform: [ "", "-arm" ] 19 | runs-on: ubuntu-24.04${{ matrix.platform }} 20 | 21 | permissions: 22 | contents: read 23 | packages: write 24 | attestations: write 25 | id-token: write 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | 31 | - name: Log in to the Container registry 32 | uses: docker/login-action@v3 33 | with: 34 | registry: ${{ env.REGISTRY }} 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Extract metadata (tags, labels) for Docker 39 | id: meta 40 | uses: docker/metadata-action@v5 41 | with: 42 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 43 | labels: | 44 | org.cyrusimap.cyrus-docker.built-from=${{ github.sha }} 45 | tags: | 46 | type=schedule,enable=${{ matrix.platform == '' }} 47 | type=ref,event=branch,enable=${{ matrix.platform == '' }} 48 | type=raw,value=bookworm-dev${{ matrix.platform }},enable=${{ github.ref == format('refs/heads/{0}', 'dev') }} 49 | type=raw,value=bookworm${{ matrix.platform }},enable=${{ github.ref == format('refs/heads/{0}', 'master') }} 50 | 51 | - name: Build and push Docker image 52 | id: push 53 | uses: docker/build-push-action@v6 54 | with: 55 | context: Debian 56 | push: true 57 | tags: ${{ steps.meta.outputs.tags }} 58 | labels: ${{ steps.meta.outputs.labels }} 59 | 60 | - name: Generate artifact attestation 61 | uses: actions/attest-build-provenance@v2 62 | with: 63 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} 64 | subject-digest: ${{ steps.push.outputs.digest }} 65 | push-to-registry: false 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /.vimrc: -------------------------------------------------------------------------------- 1 | set shiftwidth=4 2 | set tabstop=4 3 | set expandtab 4 | 5 | autocmd BufNewFile,BufRead /srv/cyrus-imapd.git/*/*.{c,h} set tabstop=8 softtabstop=4 shiftwidth=4 list listchars=tab:>. noexpandtab 6 | autocmd BufNewFile,BufRead /srv/cyrus-imapd.git/cunit/cunit.pl set tabstop=8 softtabstop=4 shiftwidth=4 list listchars=tab:>. noexpandtab 7 | autocmd BufNewFile,BufRead /srv/cyrus-imapd.git/configure.ac set tabstop=8 shiftwidth=8 noexpandtab 8 | 9 | -------------------------------------------------------------------------------- /Debian/Dockerfile: -------------------------------------------------------------------------------- 1 | # If you change the default source image, update the README, which references 2 | # the underlying Debian version. 3 | ARG DEBIAN_VERSION=bookworm 4 | FROM debian:$DEBIAN_VERSION 5 | LABEL org.opencontainers.image.authors="Cyrus IMAP " 6 | 7 | LABEL org.cyrusimap.cyrus-docker.version="1.0" 8 | 9 | RUN <run; 22 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker.pm: -------------------------------------------------------------------------------- 1 | package Cyrus::Docker; 2 | use v5.36.0; 3 | 4 | use JSON::XS (); 5 | use Path::Tiny (); 6 | 7 | use App::Cmd::Setup 0.336 -app => { 8 | getopt_conf => [], 9 | }; 10 | 11 | sub repo_root ($self) { 12 | $self->{root} //= do { 13 | my $path = $ENV{CYRUS_CLONE_ROOT} || '/srv/cyrus-imapd'; 14 | Path::Tiny::path($path); 15 | }; 16 | } 17 | 18 | sub config ($self) { 19 | $self->{config} //= do { 20 | my $path = Path::Tiny::path('/etc/cyrus-docker.json'); 21 | my $config = $path->exists ? JSON::XS::decode_json($path->slurp) : {}; 22 | 23 | if (defined $config->{default_jobs} && $config->{default_jobs} !~ /\A[0-9]+\z/) { 24 | die "$path has a default_jobs option but it isn't an integer\n"; 25 | } 26 | 27 | $config; 28 | }; 29 | } 30 | 31 | 1; 32 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker/Command/build.pm: -------------------------------------------------------------------------------- 1 | use v5.36.0; 2 | 3 | package Cyrus::Docker::Command::build; 4 | use Cyrus::Docker -command; 5 | 6 | use Process::Status; 7 | use Term::ANSIColor qw(colored); 8 | 9 | my sub run (@args) { 10 | say "running: @args"; 11 | system(@args); 12 | Process::Status->assert_ok($args[0]); 13 | } 14 | 15 | sub abstract { 'configure, build, and install cyrus-imapd' } 16 | 17 | sub opt_spec { 18 | return ( 19 | [ 'recompile|r', 'recompile, make check, and install a previous build' ], 20 | [ 'cunit!', "run make check [-n to disable]", { default => 1 } ], 21 | [ 'n', "hidden", { implies => { cunit => 0 } } ], 22 | [ 'with-sphinx|s', 'enable sphinx docs' ], 23 | [ 'jobs|j=i', 'specify number of parallel jobs (default: 8) to run for make/make check', 24 | { default => 8 }, 25 | ], 26 | [ 'sanitizer' => hidden => { one_of => [ 27 | [ 'asan' => 'build with AddressSanitizer' ], 28 | [ 'ubsan' => 'build with UBSan' ], 29 | [ 'ubsan-trap' => 'build with UBSan and trap on error' ], 30 | ] } ], 31 | [ 'compiler' => hidden => { one_of => [ 32 | [ 'gcc' => 'gcc', ], 33 | [ 'clang' => 'clang', ], 34 | ] } ], 35 | [ 'cflags=s' => 'additional flags to include in CFLAGS' ], 36 | [ 'cxxflags=s' => 'additional flags to include in CXXFLAGS' ], 37 | ); 38 | } 39 | 40 | sub execute ($self, $opt, $args) { 41 | my $root = $self->app->repo_root; 42 | chdir $root or die "can't chdir to $root: $!"; 43 | 44 | $self->configure($opt) unless $opt->recompile; 45 | 46 | my @jobs = ("-j", $self->app->config->{default_jobs} // $opt->jobs); 47 | 48 | run(qw( make lex-fix ), @jobs); 49 | run(qw( make ), @jobs); 50 | run(qw( make check ), @jobs) if $opt->cunit; 51 | run(qw( sudo make install ), @jobs); 52 | run(qw( sudo make install-binsymlinks ), @jobs); 53 | run(qw( sudo cp tools/mkimap /usr/cyrus/bin/mkimap )); 54 | 55 | system('/usr/cyrus/bin/cyr_info', 'version'); 56 | } 57 | 58 | sub configure ($self, $opt) { 59 | my $version = `./tools/git-version.sh`; 60 | Process::Status->assert_ok("determining git version"); 61 | 62 | chomp $version; 63 | 64 | if ($version eq 'unknown') { 65 | die "git-version.sh can't decide what version this is; giving up!\n"; 66 | } 67 | 68 | my $with_sanitizer = $opt->sanitizer ? " with " . $opt->sanitizer : ""; 69 | 70 | my $san_flags = q{}; 71 | 72 | if ($opt->sanitizer) { 73 | if ($opt->sanitizer eq 'asan') { 74 | $san_flags = '-fsanitize=address'; 75 | 76 | my $lsan_opts = $ENV{LSAN_OPTIONS} || ""; 77 | my $dont_suppress; 78 | 79 | if ($lsan_opts) { 80 | my %opts = map { split '=', $_ } split(':', $lsan_opts); 81 | if ($opts{supressions} && $opts{supressions} ne "cunit/leaksanitizer.suppress") { 82 | warn "Warning! LSAN_OPTIONS already defines a suppressions file so ours will not be used. You may see spurious failures...\n"; 83 | $dont_suppress = 1; 84 | } 85 | } 86 | 87 | unless ($dont_suppress) { 88 | $ENV{LSAN_OPTIONS} = "$lsan_opts:suppressions=leaksanitizer.suppress"; 89 | } 90 | 91 | if (! $opt->compiler) { 92 | warn colored(['red'], "If using gcc you may need ASAN_OPTIONS=verify_asan_link_order=0 when running cassandane tests.") . "\n"; 93 | warn colored(['red'], "Alternatively, use 'cyd build --asan --gcc' and I'll configure the build appropriately") . "\n"; 94 | 95 | } elsif ($opt->compiler eq 'gcc') { 96 | # As of at least gcc 12 we need to statically link libasan or cass 97 | # tests fail with "ASan runtime does not come first..." errors 98 | $san_flags .= ' -static-libasan'; 99 | } 100 | 101 | } elsif ($opt->sanitizer =~ /\Aubsan(_trap)?\z/) { 102 | $san_flags = '-fsanitize=undefined'; 103 | 104 | $ENV{UBSAN_OPTIONS} = "print_stacktrace=1:halt_on_error=1"; 105 | 106 | if ($opt->sanitizer eq 'ubsan_trap') { 107 | $san_flags .= ' -fsanitize-undefined-trap-on-error'; 108 | } 109 | } else { 110 | die "Unknown sanitizer mode '" . $opt->sanitizer . "'?!\n"; 111 | } 112 | } 113 | 114 | my $with_cc = ""; 115 | 116 | if ($opt->compiler) { 117 | $ENV{CC} = $opt->compiler; 118 | 119 | $with_cc = " using $ENV{CC}"; 120 | } 121 | 122 | say "building cyrusversion $version$with_cc$with_sanitizer"; 123 | 124 | my @configopts = qw( 125 | --enable-autocreate 126 | --enable-backup 127 | --enable-calalarmd 128 | --enable-gssapi 129 | --enable-http 130 | --enable-idled 131 | --enable-murder 132 | --enable-nntp 133 | --enable-replication 134 | --enable-shared 135 | --enable-silent-rules 136 | --enable-debug-slowio 137 | --enable-unit-tests 138 | --enable-xapian 139 | --enable-jmap 140 | --with-ldap=/usr" 141 | ); 142 | 143 | push @configopts, '--with-sphinx-build=no' unless $opt->with_sphinx; 144 | 145 | my $libsdir = '/usr/local/cyruslibs'; 146 | my $target = '/usr/cyrus'; 147 | 148 | my $more_cflags = $opt->cflags // ""; 149 | my $more_cxxflags = $opt->cxxflags // ""; 150 | 151 | local $ENV{LDFLAGS} = "-L$libsdir/lib/x86_64-linux-gnu -L$libsdir/lib -Wl,-rpath,$libsdir/lib/x86_64-linux-gnu -Wl,-rpath,$libsdir/lib"; 152 | local $ENV{PKG_CONFIG_PATH} = "$libsdir/lib/x86_64-linux-gnu/pkgconfig:$libsdir/lib/pkgconfig:\$PKG_CONFIG_PATH"; 153 | local $ENV{CFLAGS} = "$san_flags -g -fPIC -W -Wall -Wextra -Werror -Wwrite-strings -Wformat=2 $more_cflags"; 154 | local $ENV{CXXFLAGS} = "$san_flags -g -fPIC -W -Wall -Wextra -Werror $more_cxxflags"; 155 | local $ENV{PATH} = "$libsdir/bin:$ENV{PATH}"; 156 | 157 | run(qw( autoreconf -v -i )); 158 | 159 | run( 160 | './configure', 161 | "--prefix=$target", 162 | @configopts, 163 | "XAPIAN_CONFIG=$libsdir/bin/xapian-config-1.5", 164 | ); 165 | } 166 | 167 | 1; 168 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker/Command/clean.pm: -------------------------------------------------------------------------------- 1 | use v5.36.0; 2 | 3 | package Cyrus::Docker::Command::clean; 4 | use Cyrus::Docker -command; 5 | 6 | use Process::Status; 7 | 8 | sub abstract { 'clean all build artifacts in the cyrus-imapd source tree' } 9 | 10 | sub execute ($self, $opt, $args) { 11 | my $root = $self->app->repo_root; 12 | chdir $root or die "can't chdir to $root: $!"; 13 | 14 | # -d: recurses into untracked directories 15 | # -X: only delete files we ignore, so we don't delete new .c files (e.g.) 16 | # -f: actually delete things 17 | system(qw( git clean -dfX )); 18 | Process::Status->assert_ok("git clean"); 19 | } 20 | 21 | 1; 22 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker/Command/clone.pm: -------------------------------------------------------------------------------- 1 | use v5.36.0; 2 | 3 | package Cyrus::Docker::Command::clone; 4 | use Cyrus::Docker -command; 5 | 6 | use Path::Tiny; 7 | use Process::Status; 8 | 9 | sub abstract { 'clone the cyrus-imapd source tree to /srv, if not present' } 10 | 11 | sub execute ($self, $opt, $arg) { 12 | my $root = '/srv'; 13 | my $repo = 'https://github.com/cyrusimap/cyrus-imapd.git'; 14 | 15 | # Not yet initialized. Clone! 16 | if (-d "$root/cyrus-imapd") { 17 | say "$root/cyrus-imapd already exists, not cloning"; 18 | return; 19 | } 20 | 21 | system(qw(git clone -o github), $repo); 22 | Process::Status->assert_ok("cloning $repo"); 23 | } 24 | 25 | 1; 26 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker/Command/idle.pm: -------------------------------------------------------------------------------- 1 | use v5.36.0; 2 | 3 | package Cyrus::Docker::Command::idle; 4 | use Cyrus::Docker -command; 5 | 6 | use Process::Status; 7 | use Term::ANSIColor qw(colored); 8 | 9 | sub abstract { 'sleep forever, to keep a container running' } 10 | 11 | sub execute ($self, $opt, $args) { 12 | my $motd = <<~'END'; 13 | ///// |||| Cyrus IMAP docker image 14 | ///// |||| IDLE mode (not to be confused with IMAP IDLE) 15 | ///// |||| 16 | ///// |||| If you're seeing this, that's weird. 17 | ///// |||| 18 | \\\\\ |||| IDLE mode is most useful for a detached 19 | \\\\\ |||| container in which you exec more commands later. 20 | \\\\\ |||| 21 | \\\\\ |||| 22 | \\\\\ |||| 23 | END 24 | 25 | $motd =~ s{([/|\\]+)}{colored(['bright_cyan'], "$1")}ge; 26 | $motd =~ s{• \K([^-]+)}{colored(['bright_yellow'], "$1")}ge; 27 | print $motd; 28 | 29 | sleep 60 while 1; 30 | } 31 | 32 | 1; 33 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker/Command/makedocs.pm: -------------------------------------------------------------------------------- 1 | use v5.36.0; 2 | 3 | package Cyrus::Docker::Command::makedocs; 4 | use Cyrus::Docker -command; 5 | 6 | use Process::Status; 7 | 8 | sub abstract { 'make the docs site using Sphinx' } 9 | 10 | sub execute ($self, $opt, $args) { 11 | my $root = $self->app->repo_root->child('docsrc'); 12 | chdir $root or die "can't chdir to $root: $!"; 13 | 14 | # I would prefer to use long form options, but they are not added until 15 | # Sphinx v7, and we are using v5 right now. -- rjbs, 2025-01-10 16 | # 17 | # -n is "--nitpicky" 18 | # -W is "--fail-on-warning" 19 | system('make', q{SPHINXOPTS=-n -W}, 'html'); 20 | Process::Status->assert_ok('making "html" target'); 21 | } 22 | 23 | 1; 24 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker/Command/shell.pm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use v5.36.0; 3 | 4 | use utf8; 5 | 6 | package Cyrus::Docker::Command::shell; 7 | use Cyrus::Docker -command; 8 | use Term::ANSIColor qw(colored); 9 | 10 | sub abstract { 'run a shell' } 11 | 12 | sub command_names ($self, @rest) { 13 | my @names = $self->SUPER::command_names(@rest); 14 | return (@names, 'sh'); 15 | } 16 | 17 | sub do_motd { 18 | my $menu = <<~'END'; 19 | ///// |||| Cyrus IMAP docker image 20 | ///// |||| Run cyrus-docker (or "cyd") as: 21 | ///// |||| 22 | ///// |||| • cyd clone - clone cyrus-imapd.git from GitHub 23 | ///// |||| • cyd build - build your checked out cyrus-imapd 24 | \\\\\ |||| • cyd test - run the cyrus-imapd test suite 25 | \\\\\ |||| • cyd smoke - check out, build and test 26 | \\\\\ |||| 27 | \\\\\ |||| • cyd shell - run a shell in the container 28 | \\\\\ |||| 29 | END 30 | 31 | $menu =~ s{([/|\\]+)}{colored(['bright_cyan'], "$1")}ge; 32 | $menu =~ s{• \K([^-]+)}{colored(['bright_yellow'], "$1")}ge; 33 | print $menu; 34 | 35 | # -t *STDOUT -- detect that we have a tty 36 | # I have not yet found a reliable test for "is interactive" (-i). 37 | unless (-t *STDOUT) { 38 | say "❗️ It looks like you ran this from a non-interactive container."; 39 | say "❗️ You probably want to use: docker run -ti [image]"; 40 | exit; 41 | } 42 | } 43 | 44 | sub execute ($self, $opt, $args) { 45 | $self->do_motd; 46 | exec 'bash'; 47 | } 48 | 49 | 1; 50 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker/Command/smoke.pm: -------------------------------------------------------------------------------- 1 | use v5.36.0; 2 | 3 | package Cyrus::Docker::Command::smoke; 4 | use Cyrus::Docker -command; 5 | 6 | use Process::Status; 7 | 8 | sub abstract { 'build and test the contents of cyrus-imapd repo' } 9 | 10 | sub execute ($self, $opt, $args) { 11 | my @classes = qw( 12 | Cyrus::Docker::Command::clone 13 | Cyrus::Docker::Command::clean 14 | Cyrus::Docker::Command::build 15 | Cyrus::Docker::Command::test 16 | ); 17 | 18 | for my $class (@classes) { 19 | my ($cmd, $opt, @args) = $class->prepare($self->app); 20 | $self->app->execute_command($cmd, $opt, @args); 21 | } 22 | } 23 | 24 | 1; 25 | -------------------------------------------------------------------------------- /Debian/bin/lib/Cyrus/Docker/Command/test.pm: -------------------------------------------------------------------------------- 1 | use v5.36.0; 2 | 3 | package Cyrus::Docker::Command::test; 4 | use Cyrus::Docker -command; 5 | 6 | use Path::Tiny; 7 | use Process::Status; 8 | 9 | sub abstract { 'test the cyrus-imapd repo with cassandane' } 10 | 11 | sub opt_spec { 12 | return ( 13 | [ 'format=s', "which formatter to use; default: pretty", 14 | { default => 'pretty' } ], 15 | [ 'slow!', "run slow tests", { default => 0 } ], 16 | [ 'rerun', "only run previously-failed tests" ], 17 | [ 'jobs|j=i', "number of parallel jobs (default: 8) to run for make and testrunner", 18 | { default => 8 } ], 19 | ); 20 | } 21 | 22 | sub execute ($self, $opt, $args) { 23 | unless (-e '/run/rsyslogd.pid') { 24 | system('/usr/sbin/rsyslogd'); 25 | Process::Status->assert_ok('starting rsyslog'); 26 | } 27 | 28 | my $root = $self->app->repo_root->child('cassandane'); 29 | chdir $root or die "can't chdir to $root: $!"; 30 | 31 | unless (-e "cassandane.ini") { 32 | system(qw(cp -af cassandane.ini.dockertests cassandane.ini)); 33 | Process::Status->assert_ok('copying cassandane.ini.dockertests to cassandane.ini'); 34 | 35 | system(qw(chown cyrus:mail cassandane.ini)); 36 | Process::Status->assert_ok('chowning cassandane.ini'); 37 | } 38 | 39 | # XXX This is transitional, while we haven't updated cyrus-imap.git to 40 | # eliminate the .git in path names that existed prior to recent commits. 41 | { 42 | my @lines = path('cassandane.ini')->lines; 43 | s{/srv/[-A-Za-z]+\K.git}{}g for @lines; 44 | path('cassandane.ini')->spew(@lines); 45 | } 46 | 47 | my @jobs = ("-j", $self->app->config->{default_jobs} // $opt->jobs); 48 | 49 | system(qw(make), @jobs); 50 | Process::Status->assert_ok('Cassandane make'); 51 | 52 | # The idea here is that if the user ran "cyd test Some::Test" then running 53 | # "make syntax" could add a lot of overhead in syntax checking. If they're 54 | # testing *everything*, though, or "everything but three tests", then running 55 | # a syntax check is a good idea. The --rerun options is treated like a 56 | # specific test selection, which is a bit of a gamble, but probably a good 57 | # one. 58 | my $selects_tests = $opt->rerun || grep {; !/^!/ && !/^-/ } @$args; 59 | unless ($selects_tests) { 60 | system(qw(make syntax), @jobs); 61 | Process::Status->assert_ok('Cassandane make syntax'); 62 | } 63 | 64 | system( 65 | qw( setpriv --reuid=cyrus --regid=mail --clear-groups --inh-caps=-all ), 66 | qw( ./testrunner.pl ), @jobs, qw( -f ), $opt->format, 67 | ($opt->rerun ? '--rerun' : ()), 68 | ($opt->slow ? '--slow' : ()), 69 | @$args, 70 | ); 71 | 72 | Process::Status->assert_ok('Cassandane run'); 73 | } 74 | 75 | 1; 76 | -------------------------------------------------------------------------------- /Debian/dot.bashrc: -------------------------------------------------------------------------------- 1 | # .bashrc 2 | 3 | # Source global definitions 4 | if [ -f /etc/bashrc ]; then 5 | . /etc/bashrc 6 | fi 7 | 8 | if [ -f /etc/bash_completion ]; then 9 | . /etc/bash_completion 10 | fi 11 | 12 | alias ls='ls $LS_OPTIONS' 13 | alias ll='ls $LS_OPTIONS -l' 14 | alias l='ls $LS_OPTIONS -lA' 15 | 16 | # Some more alias to avoid making mistakes: 17 | alias rm='rm -i' 18 | alias cp='cp -i' 19 | alias mv='mv -i' 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Images for Cyrus IMAP 2 | 3 | This repo contains a Dockerfile for building a container that has all the 4 | required libraries for building and testing Cyrus IMAP. It is meant for use in 5 | Cyrus IMAP's automated test runs, and for testing changes while developing 6 | Cyrus. 7 | 8 | There are two ways to acquire the Docker images. 9 | 10 | ## Build locally from a Dockerfile 11 | 12 | Debian is the preferred platform for Cyrus IMAP, so these instructions will be 13 | specific to Debian distributions. While we'd like to support multiple 14 | platforms in the future, we do not currently do so. 15 | 16 | The `Dockerfile` is in the `Debian` directory. To build the Debian 17 | based Docker image, run the following commands from the current 18 | directory: 19 | 20 | ``` 21 | $ cd Debian 22 | $ docker build -t . 23 | ``` 24 | 25 | where `` could be anything you like. Because the current Docker 26 | image is based on [Debian 27 | "bookworm"](https://www.debian.org/releases/bookworm/), we would typically run 28 | it as: 29 | 30 | ``` 31 | $ docker build -t cyrus-bookworm . 32 | ``` 33 | 34 | ..and let Docker do its thing. 35 | 36 | ## Fetch latest images from the GitHub Container Repository 37 | 38 | ``` 39 | $ docker pull ghcr.io/cyrusimap/cyrus-docker:nightly 40 | ``` 41 | 42 | 43 | ## Running the Docker instance 44 | 45 | To run the built container: 46 | 47 | ``` 48 | $ docker run -it ghcr.io/cyrusimap/cyrus-docker:nightly 49 | ``` 50 | 51 | (Or provide whatever name you use when building the image yourself.) 52 | 53 | You'll be dropped into an interactive shell with some help about how to go 54 | about cloning and testing Cyrus IMAP. You can also look at the included `dar` 55 | tool (`./bin/dar)) for how to use this container while working on your 56 | own branch of Cyrus IMAP. 57 | -------------------------------------------------------------------------------- /bin/dar: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use v5.20.0; # I just want signatures. 3 | use warnings; 4 | use experimental 'signatures'; # Everything we use is stable and accepted. 5 | 6 | package Cyrus::Docker::dar; 7 | 8 | use utf8; 9 | 10 | BEGIN { 11 | my @missing; 12 | for my $prereq ( 13 | 'IPC::Run', 14 | 'Path::Tiny', 15 | 'Process::Status', 16 | ) { 17 | my $ok = eval "require $prereq; 1"; 18 | push @missing, $prereq unless $ok; 19 | } 20 | 21 | if (@missing) { 22 | my $error = join qq{\n}, ( 23 | "You're missing some required modules, please install them:", 24 | map {; " $_" } @missing, 25 | ); 26 | 27 | die "$error\n"; 28 | } 29 | } 30 | 31 | # Core 32 | use Digest::SHA qw(sha1_hex); 33 | use Getopt::Long (); 34 | use JSON::PP; 35 | use Term::ANSIColor qw(colored); 36 | 37 | # CPAN 38 | use IPC::Run qw(run); 39 | use Path::Tiny 0.125; 40 | use Process::Status; 41 | 42 | my $MINIMUM_IMAGE_VERSION = 1; 43 | 44 | binmode *STDOUT, ':encoding(utf-8)'; 45 | binmode *STDERR, ':encoding(utf-8)'; 46 | 47 | my $is_tty = -t *STDOUT; 48 | 49 | unless ($is_tty) { 50 | # Why //=? So you can, in a pinch, undisable them without a TTY by setting 51 | # it to a 0. 52 | $ENV{ANSI_COLORS_DISABLED} //= 1; 53 | } 54 | 55 | my $MENU = <<'END'; 56 | dar: the cyrus-docker dev tool 57 | 58 | Run "dar COMMAND". Here are some commands: 59 | • pull - pull the latest docker container image 60 | • start - start a container to build in the current dir 61 | • prune - stop and destroy the container for this dir 62 | • help - get more information about how to use dar 63 | • run - run the given command in the container 64 | • cyd - a shortcut for "dar run cyd ..." 65 | 66 | And these commands all run "dar cyd CMD" in the container: 67 | • build - configure and compile Cyrus 68 | • clean - "make clean" the repo 69 | • makedocs - build the html version of the docs 70 | • shell - run a shell 71 | • smoke - sugar for "build then test" 72 | • test - run the Cassandane test suite 73 | END 74 | 75 | $MENU =~ s{([/|\\]+)}{colored(['bright_blue'], "$1")}ge; 76 | $MENU =~ s{• \K([^-]+)}{colored(['bright_yellow'], "$1")}ge; 77 | 78 | my $HELP = <<'END'; 79 | dar helps manage Docker containers for building and testing cyrus-imapd, using 80 | a copy of the source stored on your computer. 81 | 82 | To get a container running… 83 | 84 | 1. cd to your local clone of cyrus-imapd 85 | 2. run "dar pull" to make sure you have the latest cyrus-docker image 86 | 3. run "dar start" to get a running container 87 | 88 | Once you have that, you generally run "dar XYZ" to run "cyd XYZ" inside the 89 | container. This is most useful for: build, test, smoke, and sh. You can also 90 | use "dar run ..." to run that "..." in the container, for non-cyd commands. 91 | 92 | When you want to clean up the container you've got running, run "dar prune". 93 | 94 | The default image is ghcr.io/cyrusimap/cyrus-docker:bookworm (or bookworm-arm 95 | on ARM64), but you can set a different default for all your uses of "dar" by 96 | creating the file ~/.cyrus-docker/config, which should contain a JSON object. 97 | The only meaningful key, for now, is "default_image", which provides an 98 | alternate default image. 99 | END 100 | 101 | my $command = @ARGV ? shift(@ARGV) : 'commands'; 102 | 103 | # turn --help into help, and support old-style "--prune" etc. 104 | $command =~ s/\A--//; 105 | 106 | my $ABS_CWD = path('.')->absolute; 107 | 108 | my $CONFIG = Cyrus::Docker::dar->load_config(); 109 | 110 | my $method = __PACKAGE__->can("do_" . $command); 111 | 112 | unless ($method) { 113 | die qq{❌ Unknown command requested: "$command". Try "dar help".\n}; 114 | } 115 | 116 | __PACKAGE__->$method([@ARGV]); 117 | 118 | sub _emptyref { 119 | my $str = q{}; 120 | return \$str; 121 | } 122 | 123 | sub do_help { 124 | print $HELP; 125 | return; 126 | } 127 | 128 | sub do_commands { 129 | print $MENU; 130 | return; 131 | } 132 | 133 | sub do_pull ($class, $args) { 134 | # Generally, this should not fail..? 135 | die "error parsing arguments!\n" unless Getopt::Long::GetOptionsFromArray( 136 | $args, 137 | 'image=s' => \my $opt_image, 138 | ); 139 | 140 | my $image_specifier = $class->_requested_image($opt_image); 141 | 142 | system('docker', 'pull', $image_specifier); 143 | $? && die "❌ Error fetching image $image_specifier\n"; 144 | 145 | say "✅ Container image up to date."; 146 | return; 147 | } 148 | 149 | sub do_start ($class, $args) { 150 | # Generally, this should not fail..? 151 | die "error parsing arguments!\n" unless Getopt::Long::GetOptionsFromArray( 152 | $args, 153 | 'keep' => \my $opt_keep, 154 | 'image=s' => \my $opt_image, 155 | 'run-outside-clone' => \my $run_outside_clone, 156 | ); 157 | 158 | # [ 'keep', 'keep the container after exit' ], 159 | # [ 'image=s', 'which image to use' ], 160 | # [ 'run-outside-clone', 'run even if cwd is not a cyrus-imapd clone' ], 161 | 162 | unless (-e 'imap/imapd.c' || $run_outside_clone) { 163 | die <<'END'; 164 | The current directory doesn't appear to be a cyrus-imapd clone. To run dar 165 | anyway, pass the --run-outside-clone switch. 166 | END 167 | } 168 | 169 | my $existing_container = $class->_existing_container; 170 | 171 | if ($existing_container) { 172 | unless ($existing_container->{State} eq 'exited') { 173 | # There are states other than running and exited, but we're going to 174 | # treat anything but "exited" as "still running" for now. 175 | die "❌ The container $existing_container->{Names} is already running!\n"; 176 | } 177 | 178 | if ($existing_container->{Command} ne q{"cyd idle"}) { 179 | # I don't think this should ever happen either... 180 | die "❌ Weird: existing container isn't set to run cyd idle. Giving up.\n"; 181 | } 182 | 183 | say "⏳ Restarting container $existing_container->{Names} to idle."; 184 | run( 185 | [ 'docker', 'start', $existing_container->{ID} ], 186 | _emptyref(), 187 | \my $container_id, 188 | ); 189 | 190 | Process::Status->assert_ok("❌ Restarting container"); 191 | return $existing_container; 192 | } 193 | 194 | my $name = $class->container_name_for_cwd; 195 | say "⏳ Starting container $name to idle."; 196 | 197 | my $image_specifier = $class->_requested_image($opt_image); 198 | 199 | { 200 | # Assert that we have the image. If not, point user to "dar pull" 201 | run( 202 | [ 'docker', 'image', 'ls', '--format', 'json', $image_specifier ], 203 | _emptyref(), 204 | \my $image_json_lines, 205 | ); 206 | 207 | Process::Status->assert_ok("❌ Getting list of available images"); 208 | 209 | chomp $image_json_lines; 210 | my @lines = split /\n/, $image_json_lines; 211 | 212 | @lines == 0 213 | && die qq{❌ The image $image_specifier isn't available. Maybe you should "dar pull".\n}; 214 | 215 | @lines > 1 216 | && die qq{❌ $image_specifier matches more than one candidate image.\n}; 217 | } 218 | 219 | my $image = $class->_get_image($image_specifier); 220 | 221 | my $image_version = $image->{Config}{Labels}{'org.cyrusimap.cyrus-docker.version'}; 222 | 223 | unless ($image_version && $image_version >= $MINIMUM_IMAGE_VERSION) { 224 | # In the future, when we actually *use* this facility for something, we may 225 | # want to be more specific, like "you need v3 minimum" or "the following 226 | # commands will not work without v3" or whatever. For now, "just update" 227 | # seems solid. 228 | die "❌ This container is too old for this version of dar.\n"; 229 | } 230 | 231 | run( 232 | [ 233 | 'docker', 'run', 234 | '--detach', 235 | '--name', $name, 236 | '--mount', "type=bind,src=$ABS_CWD,dst=/srv/cyrus-imapd", 237 | ($opt_keep ? () : '--rm'), 238 | '--cap-add=SYS_PTRACE', 239 | $image_specifier, 240 | qw( cyd idle ) 241 | ], 242 | _emptyref(), 243 | \my $container_id, 244 | ); 245 | 246 | Process::Status->assert_ok("❌ Starting idle container"); 247 | 248 | chomp $container_id; 249 | say "✅ Container started, id: $container_id"; 250 | 251 | my $container = $class->_existing_container; 252 | 253 | unless ($container) { 254 | # This is another one of those "should never happen" things… 255 | die "❌ The container was started, but now can't be found!\n" 256 | } 257 | 258 | # We need the git-version.sh program to work, which means that "git describe" 259 | # needs to work in the container's git repo, but it will be running as root, 260 | # so git will complain about mismatched ownership unless we mark this 261 | # directory safe. -- rjbs, 2024-12-27 262 | run([ 263 | qw( docker exec ), $container->{ID}, 264 | qw( git config --global --add safe.directory /srv/cyrus-imapd ), 265 | ]); 266 | 267 | Process::Status->assert_ok("❌ Fixing git permissions in container"); 268 | 269 | my $config_file = path('~/.cyrus-docker/config'); 270 | if (-e $config_file) { 271 | run( 272 | [ 273 | 'docker', 'cp', '--quiet', 274 | $config_file->absolute, 275 | "$container_id:/etc/cyrus-docker.json", 276 | ], 277 | _emptyref(), 278 | _emptyref(), 279 | ); 280 | 281 | if ($?) { 282 | warn "❗️ Couldn't copy config into container, " . 283 | Process::Status->new($?)->as_string; 284 | } 285 | } 286 | 287 | return $class->_existing_container; 288 | } 289 | 290 | sub do_prune ($class, $args) { 291 | @$args && die "❌ You can't supply a command to run with --prune.\n"; 292 | 293 | my $container = $class->_existing_container; 294 | 295 | unless ($container) { 296 | say "✅ Nothing to clean up."; 297 | return; 298 | } 299 | 300 | run( 301 | [ qw( docker inspect ), $container->{ID} ], 302 | _emptyref(), 303 | \my $inspect_json, 304 | ); 305 | 306 | Process::Status->assert_ok("❌ Inspecting stopped container"); 307 | 308 | my $inspect = decode_json($inspect_json); 309 | my $autoremove = $inspect->[0]{HostConfig}{AutoRemove}; 310 | 311 | run( 312 | [ qw( docker container stop ), $container->{ID} ], 313 | _emptyref(), 314 | _emptyref(), 315 | ); 316 | 317 | Process::Status->assert_ok("❌ Stopping existing container"); 318 | 319 | say "✅ Container stopped."; 320 | 321 | unless ($autoremove) { 322 | run([ qw( docker container rm ), $container->{ID} ]); 323 | Process::Status->assert_ok("❌ Removing stopped container"); 324 | } 325 | } 326 | 327 | BEGIN { 328 | for my $cyd_cmd (qw( build clean makedocs shell smoke test )) { 329 | my $code = sub ($class, $args) { 330 | $class->do_run([ 'cyd', $cyd_cmd, @$args ]); 331 | }; 332 | 333 | no strict 'refs'; 334 | *{"do_$cyd_cmd"} = $code; 335 | } 336 | 337 | { 338 | no warnings 'once'; 339 | *do_sh = \&do_shell; 340 | } 341 | } 342 | 343 | sub do_clone ($class, $args) { 344 | die "❌ clone is a cyd command, but not a dar command.\n"; 345 | } 346 | 347 | sub do_cyd ($class, $args) { 348 | $class->do_run([ 'cyd', @$args ]); 349 | } 350 | 351 | sub do_run ($class, $args) { 352 | my $container = $class->_existing_container; 353 | 354 | unless ($container && $container->{State} eq 'running') { 355 | die qq{❌ You don't have a running container. You'll want to run "dar start".\n}; 356 | } 357 | 358 | if ($container->{Command} ne q{"cyd idle"}) { 359 | # I don't think this should ever happen either... 360 | die "❌ Weird: existing container isn't running cyd idle. Giving up.\n"; 361 | } 362 | 363 | say "⏳ Executing command in container $container->{ID}..."; 364 | 365 | exec( 366 | qw( docker exec --workdir /srv/cyrus-imapd -ti ), 367 | $container->{ID}, 368 | @$args, 369 | ); 370 | } 371 | 372 | sub _requested_image ($class, $opt_image) { 373 | state $uname = `uname -a`; 374 | state $suffix = $uname =~ /\barm64\b/ ? '-arm' : q{}; 375 | 376 | return $opt_image 377 | // $CONFIG->{default_image} 378 | // "ghcr.io/cyrusimap/cyrus-docker:bookworm$suffix"; 379 | } 380 | 381 | sub _get_image ($class, $image_specifier) { 382 | run( 383 | [ 'docker', 'image', 'inspect', $image_specifier ], 384 | _emptyref(), 385 | \my $json, 386 | ); 387 | 388 | Process::Status->assert_ok("❌ Inspecting image"); 389 | 390 | my $data = decode_json($json); 391 | 392 | if (@$data > 1) { 393 | die "❌ More than one image description came back from docker image inspect?!\n"; 394 | } 395 | 396 | return $data->[0]; 397 | } 398 | 399 | sub _get_containers { 400 | my %container_named = do { 401 | my (@lines) = `docker container list -a --format json`; 402 | 403 | Process::Status->assert_ok("❌ Getting container list"); 404 | chomp @lines; 405 | 406 | # Names? Plural? I'm gonna guess that if you do weird things you can get 407 | # "name1,name2" but for now I will not worry about it -- rjbs, 2024-12-24 408 | map {; $_->{Names} => $_ } map { decode_json($_) } @lines; 409 | }; 410 | 411 | return \%container_named; 412 | } 413 | 414 | sub _existing_container ($class) { 415 | my $containers = $class->_get_containers; 416 | return $containers->{ $class->container_name_for_cwd }; 417 | } 418 | 419 | sub container_name_for_cwd { 420 | my $digest = sha1_hex("$ABS_CWD"); 421 | return "cyd-" . substr($digest, 0, 12); 422 | } 423 | 424 | sub load_config { 425 | my $config_file = path('~/.cyrus-docker')->mkdir->child('config'); 426 | return {} unless -e $config_file; 427 | return decode_json($config_file->slurp); 428 | } 429 | -------------------------------------------------------------------------------- /fatpacked/.gitignore: -------------------------------------------------------------------------------- 1 | /fatlib 2 | /fatpacker.trace 3 | /packlists 4 | -------------------------------------------------------------------------------- /fatpacked/Makefile: -------------------------------------------------------------------------------- 1 | dar: ../bin/dar 2 | fatpack packlists-for IPC/Run.pm Path/Tiny.pm Process/Status.pm > packlists 3 | fatpack tree `cat packlists` 4 | fatpack file ../bin/dar > dar 5 | chmod u+x dar 6 | 7 | clean: 8 | rm -f dar 9 | rm -f packlists 10 | rm -f fatpacker.trace 11 | --------------------------------------------------------------------------------