Home | History | Annotate | Line # | Download | only in scripts
      1  1.1  christos #!/usr/bin/env python3
      2  1.1  christos 
      3  1.1  christos from itertools import combinations, chain
      4  1.1  christos from enum import Enum, auto
      5  1.1  christos 
      6  1.1  christos 
      7  1.1  christos LINUX = 'ubuntu-24.04'
      8  1.1  christos OSX = 'macos-latest'
      9  1.1  christos WINDOWS = 'windows-latest'
     10  1.1  christos FREEBSD = 'freebsd'
     11  1.1  christos 
     12  1.1  christos AMD64 = 'amd64'
     13  1.1  christos ARM64 = 'arm64'
     14  1.1  christos PPC64LE = 'ppc64le'
     15  1.1  christos 
     16  1.1  christos 
     17  1.1  christos GITHUB_ACTIONS_TEMPLATE = """\
     18  1.1  christos # This config file is generated by ./scripts/gen_gh_actions.py.
     19  1.1  christos # Do not edit by hand.
     20  1.1  christos 
     21  1.1  christos name: {name}
     22  1.1  christos 
     23  1.1  christos on:
     24  1.1  christos   push:
     25  1.1  christos     branches: [ dev, ci_travis ]
     26  1.1  christos   pull_request:
     27  1.1  christos     branches: [ dev ]
     28  1.1  christos 
     29  1.1  christos jobs:
     30  1.1  christos {jobs}
     31  1.1  christos """
     32  1.1  christos 
     33  1.1  christos 
     34  1.1  christos class Option(object):
     35  1.1  christos     class Type:
     36  1.1  christos         COMPILER = auto()
     37  1.1  christos         COMPILER_FLAG = auto()
     38  1.1  christos         CONFIGURE_FLAG = auto()
     39  1.1  christos         MALLOC_CONF = auto()
     40  1.1  christos         FEATURE = auto()
     41  1.1  christos 
     42  1.1  christos     def __init__(self, type, value):
     43  1.1  christos         self.type = type
     44  1.1  christos         self.value = value
     45  1.1  christos 
     46  1.1  christos     @staticmethod
     47  1.1  christos     def as_compiler(value):
     48  1.1  christos         return Option(Option.Type.COMPILER, value)
     49  1.1  christos 
     50  1.1  christos     @staticmethod
     51  1.1  christos     def as_compiler_flag(value):
     52  1.1  christos         return Option(Option.Type.COMPILER_FLAG, value)
     53  1.1  christos 
     54  1.1  christos     @staticmethod
     55  1.1  christos     def as_configure_flag(value):
     56  1.1  christos         return Option(Option.Type.CONFIGURE_FLAG, value)
     57  1.1  christos 
     58  1.1  christos     @staticmethod
     59  1.1  christos     def as_malloc_conf(value):
     60  1.1  christos         return Option(Option.Type.MALLOC_CONF, value)
     61  1.1  christos 
     62  1.1  christos     @staticmethod
     63  1.1  christos     def as_feature(value):
     64  1.1  christos         return Option(Option.Type.FEATURE, value)
     65  1.1  christos 
     66  1.1  christos     def __eq__(self, obj):
     67  1.1  christos         return (isinstance(obj, Option) and obj.type == self.type
     68  1.1  christos                 and obj.value == self.value)
     69  1.1  christos 
     70  1.1  christos     def __repr__(self):
     71  1.1  christos         type_names = {
     72  1.1  christos             Option.Type.COMPILER: 'COMPILER',
     73  1.1  christos             Option.Type.COMPILER_FLAG: 'COMPILER_FLAG',
     74  1.1  christos             Option.Type.CONFIGURE_FLAG: 'CONFIGURE_FLAG',
     75  1.1  christos             Option.Type.MALLOC_CONF: 'MALLOC_CONF',
     76  1.1  christos             Option.Type.FEATURE: 'FEATURE'
     77  1.1  christos         }
     78  1.1  christos         return f"Option({type_names[self.type]}, {repr(self.value)})"
     79  1.1  christos 
     80  1.1  christos 
     81  1.1  christos # The 'default' configuration is gcc, on linux, with no compiler or configure
     82  1.1  christos # flags.  We also test with clang, -m32, --enable-debug, --enable-prof,
     83  1.1  christos # --disable-stats, and --with-malloc-conf=tcache:false.  To avoid abusing
     84  1.1  christos # CI resources though, we don't test all 2**7 = 128 possible combinations of these;
     85  1.1  christos # instead, we only test combinations of up to 2 'unusual' settings, under the
     86  1.1  christos # hope that bugs involving interactions of such settings are rare.
     87  1.1  christos MAX_UNUSUAL_OPTIONS = 2
     88  1.1  christos 
     89  1.1  christos 
     90  1.1  christos GCC = Option.as_compiler('CC=gcc CXX=g++')
     91  1.1  christos CLANG = Option.as_compiler('CC=clang CXX=clang++')
     92  1.1  christos CL = Option.as_compiler('CC=cl.exe CXX=cl.exe')
     93  1.1  christos 
     94  1.1  christos 
     95  1.1  christos compilers_unusual = [CLANG,]
     96  1.1  christos 
     97  1.1  christos 
     98  1.1  christos CROSS_COMPILE_32BIT = Option.as_feature('CROSS_COMPILE_32BIT')
     99  1.1  christos feature_unusuals = [CROSS_COMPILE_32BIT]
    100  1.1  christos 
    101  1.1  christos 
    102  1.1  christos configure_flag_unusuals = [Option.as_configure_flag(opt) for opt in (
    103  1.1  christos     '--enable-debug',
    104  1.1  christos     '--enable-prof',
    105  1.1  christos     '--disable-stats',
    106  1.1  christos     '--disable-libdl',
    107  1.1  christos     '--enable-opt-safety-checks',
    108  1.1  christos     '--with-lg-page=16',
    109  1.1  christos     '--with-lg-page=16 --with-lg-hugepage=29',
    110  1.1  christos )]
    111  1.1  christos LARGE_HUGEPAGE = Option.as_configure_flag("--with-lg-page=16 --with-lg-hugepage=29")
    112  1.1  christos 
    113  1.1  christos 
    114  1.1  christos malloc_conf_unusuals = [Option.as_malloc_conf(opt) for opt in (
    115  1.1  christos     'tcache:false',
    116  1.1  christos     'dss:primary',
    117  1.1  christos     'percpu_arena:percpu',
    118  1.1  christos     'background_thread:true',
    119  1.1  christos )]
    120  1.1  christos 
    121  1.1  christos 
    122  1.1  christos all_unusuals = (compilers_unusual + feature_unusuals
    123  1.1  christos     + configure_flag_unusuals + malloc_conf_unusuals)
    124  1.1  christos 
    125  1.1  christos 
    126  1.1  christos def get_extra_cflags(os, compiler):
    127  1.1  christos     if os == WINDOWS:
    128  1.1  christos         # For non-CL compilers under Windows (for now it's only MinGW-GCC),
    129  1.1  christos         # -fcommon needs to be specified to correctly handle multiple
    130  1.1  christos         # 'malloc_conf' symbols and such, which are declared weak under Linux.
    131  1.1  christos         # Weak symbols don't work with MinGW-GCC.
    132  1.1  christos         if compiler != CL.value:
    133  1.1  christos             return ['-fcommon']
    134  1.1  christos         else:
    135  1.1  christos             return []
    136  1.1  christos 
    137  1.1  christos     # We get some spurious errors when -Warray-bounds is enabled.
    138  1.1  christos     extra_cflags = ['-Werror', '-Wno-array-bounds']
    139  1.1  christos     if compiler == CLANG.value or os == OSX:
    140  1.1  christos         extra_cflags += [
    141  1.1  christos             '-Wno-unknown-warning-option',
    142  1.1  christos             '-Wno-ignored-attributes'
    143  1.1  christos         ]
    144  1.1  christos     if os == OSX:
    145  1.1  christos         extra_cflags += [
    146  1.1  christos             '-Wno-deprecated-declarations',
    147  1.1  christos         ]
    148  1.1  christos     return extra_cflags
    149  1.1  christos 
    150  1.1  christos 
    151  1.1  christos def format_env_dict(os, arch, combination):
    152  1.1  christos     """Format environment variables as a dictionary for the matrix."""
    153  1.1  christos     compilers = [x.value for x in combination if x.type == Option.Type.COMPILER]
    154  1.1  christos     compiler_flags = [x.value for x in combination if x.type == Option.Type.COMPILER_FLAG]
    155  1.1  christos     configure_flags = [x.value for x in combination if x.type == Option.Type.CONFIGURE_FLAG]
    156  1.1  christos     malloc_conf = [x.value for x in combination if x.type == Option.Type.MALLOC_CONF]
    157  1.1  christos     features = [x.value for x in combination if x.type == Option.Type.FEATURE]
    158  1.1  christos 
    159  1.1  christos     if len(malloc_conf) > 0:
    160  1.1  christos         configure_flags.append('--with-malloc-conf=' + ','.join(malloc_conf))
    161  1.1  christos 
    162  1.1  christos     if not compilers:
    163  1.1  christos         compiler = GCC.value
    164  1.1  christos     else:
    165  1.1  christos         compiler = compilers[0]
    166  1.1  christos 
    167  1.1  christos     cross_compile = CROSS_COMPILE_32BIT.value in features
    168  1.1  christos     if os == LINUX and cross_compile:
    169  1.1  christos         compiler_flags.append('-m32')
    170  1.1  christos 
    171  1.1  christos     env_dict = {}
    172  1.1  christos 
    173  1.1  christos     # Parse compiler
    174  1.1  christos     cc_parts = compiler.split()
    175  1.1  christos     for part in cc_parts:
    176  1.1  christos         if part.startswith('CC='):
    177  1.1  christos             env_dict['CC'] = part.split('=')[1]
    178  1.1  christos         elif part.startswith('CXX='):
    179  1.1  christos             env_dict['CXX'] = part.split('=')[1]
    180  1.1  christos 
    181  1.1  christos     # Add features
    182  1.1  christos     for feature in features:
    183  1.1  christos         env_dict[feature] = 'yes'
    184  1.1  christos 
    185  1.1  christos     # Add flags
    186  1.1  christos     if compiler_flags:
    187  1.1  christos         env_dict['COMPILER_FLAGS'] = ' '.join(compiler_flags)
    188  1.1  christos     if configure_flags:
    189  1.1  christos         env_dict['CONFIGURE_FLAGS'] = ' '.join(configure_flags)
    190  1.1  christos 
    191  1.1  christos     extra_cflags = get_extra_cflags(os, compiler)
    192  1.1  christos     if extra_cflags:
    193  1.1  christos         env_dict['EXTRA_CFLAGS'] = ' '.join(extra_cflags)
    194  1.1  christos 
    195  1.1  christos     return env_dict
    196  1.1  christos 
    197  1.1  christos 
    198  1.1  christos def generate_job_matrix_entries(os, arch, exclude, max_unusual_opts, unusuals=all_unusuals):
    199  1.1  christos     """Generate matrix entries for a job."""
    200  1.1  christos     entries = []
    201  1.1  christos     for combination in chain.from_iterable(
    202  1.1  christos             [combinations(unusuals, i) for i in range(max_unusual_opts + 1)]):
    203  1.1  christos         if not any(excluded in combination for excluded in exclude):
    204  1.1  christos             env_dict = format_env_dict(os, arch, combination)
    205  1.1  christos             entries.append(env_dict)
    206  1.1  christos     return entries
    207  1.1  christos 
    208  1.1  christos 
    209  1.1  christos def generate_linux_job(arch):
    210  1.1  christos     """Generate Linux job configuration."""
    211  1.1  christos     os = LINUX
    212  1.1  christos 
    213  1.1  christos     # Only generate 2 unusual options for AMD64 to reduce matrix size
    214  1.1  christos     max_unusual_opts = MAX_UNUSUAL_OPTIONS if arch == AMD64 else 1
    215  1.1  christos 
    216  1.1  christos     exclude = []
    217  1.1  christos     if arch == PPC64LE:
    218  1.1  christos         # Avoid 32 bit builds and clang on PowerPC
    219  1.1  christos         exclude = (CROSS_COMPILE_32BIT, CLANG,)
    220  1.1  christos     if arch == ARM64:
    221  1.1  christos         # Avoid 32 bit build on ARM64
    222  1.1  christos         exclude = (CROSS_COMPILE_32BIT,)
    223  1.1  christos 
    224  1.1  christos     if arch != ARM64:
    225  1.1  christos         exclude += [LARGE_HUGEPAGE]
    226  1.1  christos 
    227  1.1  christos     linux_configure_flags = list(configure_flag_unusuals)
    228  1.1  christos     linux_configure_flags.append(Option.as_configure_flag("--enable-prof --enable-prof-frameptr"))
    229  1.1  christos 
    230  1.1  christos     linux_unusuals = (compilers_unusual + feature_unusuals
    231  1.1  christos                     + linux_configure_flags + malloc_conf_unusuals)
    232  1.1  christos 
    233  1.1  christos     matrix_entries = generate_job_matrix_entries(os, arch, exclude, max_unusual_opts, linux_unusuals)
    234  1.1  christos 
    235  1.1  christos     arch_suffix = f"-{arch}" if arch != AMD64 else ""
    236  1.1  christos 
    237  1.1  christos     # Select appropriate runner based on architecture
    238  1.1  christos     if arch == ARM64:
    239  1.1  christos         runner = "ubuntu-24.04-arm"    # Free ARM64 runner for public repos (Public Preview)
    240  1.1  christos     elif arch == PPC64LE:
    241  1.1  christos         # GitHub doesn't provide PPC runners, would need self-hosted
    242  1.1  christos         runner = "self-hosted-ppc64le"
    243  1.1  christos     else:  # AMD64
    244  1.1  christos         runner = "ubuntu-24.04"        # Ubuntu 24.04 LTS
    245  1.1  christos 
    246  1.1  christos     job = f"""  test-linux{arch_suffix}:
    247  1.1  christos     runs-on: {runner}
    248  1.1  christos     strategy:
    249  1.1  christos       fail-fast: false
    250  1.1  christos       matrix:
    251  1.1  christos         include:
    252  1.1  christos """
    253  1.1  christos 
    254  1.1  christos     for entry in matrix_entries:
    255  1.1  christos         job += "          - env:\n"
    256  1.1  christos         for key, value in entry.items():
    257  1.1  christos             # Properly escape values with special characters
    258  1.1  christos             if ' ' in str(value) or any(c in str(value) for c in [':', ',', '#']):
    259  1.1  christos                 job += f'              {key}: "{value}"\n'
    260  1.1  christos             else:
    261  1.1  christos                 job += f"              {key}: {value}\n"
    262  1.1  christos 
    263  1.1  christos     # Add manual job entries
    264  1.1  christos     manual_entries = [
    265  1.1  christos         {
    266  1.1  christos             'CC': 'gcc',
    267  1.1  christos             'CXX': 'g++',
    268  1.1  christos             'CONFIGURE_FLAGS': '--enable-debug --disable-cache-oblivious --enable-stats --enable-log --enable-prof',
    269  1.1  christos             'EXTRA_CFLAGS': '-Werror -Wno-array-bounds'
    270  1.1  christos         },
    271  1.1  christos         {
    272  1.1  christos             'CC': 'gcc',
    273  1.1  christos             'CXX': 'g++',
    274  1.1  christos             'CONFIGURE_FLAGS': '--enable-debug --enable-experimental-smallocx --enable-stats --enable-prof',
    275  1.1  christos             'EXTRA_CFLAGS': '-Werror -Wno-array-bounds'
    276  1.1  christos         }
    277  1.1  christos     ]
    278  1.1  christos 
    279  1.1  christos     if arch == AMD64:
    280  1.1  christos         for entry in manual_entries:
    281  1.1  christos             job += "          - env:\n"
    282  1.1  christos             for key, value in entry.items():
    283  1.1  christos                 if ' ' in str(value):
    284  1.1  christos                     job += f'              {key}: "{value}"\n'
    285  1.1  christos                 else:
    286  1.1  christos                     job += f"              {key}: {value}\n"
    287  1.1  christos 
    288  1.1  christos     job += f"""
    289  1.1  christos     steps:
    290  1.1  christos     - uses: actions/checkout@v4
    291  1.1  christos 
    292  1.1  christos     - name: Show OS version
    293  1.1  christos       run: |
    294  1.1  christos         echo "=== System Information ==="
    295  1.1  christos         uname -a
    296  1.1  christos         echo ""
    297  1.1  christos         echo "=== Architecture ==="
    298  1.1  christos         uname -m
    299  1.1  christos         arch
    300  1.1  christos         echo ""
    301  1.1  christos         echo "=== OS Release ==="
    302  1.1  christos         cat /etc/os-release || true
    303  1.1  christos         echo ""
    304  1.1  christos         echo "=== CPU Info ==="
    305  1.1  christos         lscpu | grep -E "Architecture|CPU op-mode|Byte Order|CPU\(s\):" || true
    306  1.1  christos 
    307  1.1  christos     - name: Install dependencies (32-bit)
    308  1.1  christos       if: matrix.env.CROSS_COMPILE_32BIT == 'yes'
    309  1.1  christos       run: |
    310  1.1  christos         sudo dpkg --add-architecture i386
    311  1.1  christos         sudo apt-get update
    312  1.1  christos         sudo apt-get install -y gcc-multilib g++-multilib libc6-dev-i386
    313  1.1  christos 
    314  1.1  christos     - name: Build and test
    315  1.1  christos       env:
    316  1.1  christos         CC: ${{{{ matrix.env.CC }}}}
    317  1.1  christos         CXX: ${{{{ matrix.env.CXX }}}}
    318  1.1  christos         COMPILER_FLAGS: ${{{{ matrix.env.COMPILER_FLAGS }}}}
    319  1.1  christos         CONFIGURE_FLAGS: ${{{{ matrix.env.CONFIGURE_FLAGS }}}}
    320  1.1  christos         EXTRA_CFLAGS: ${{{{ matrix.env.EXTRA_CFLAGS }}}}
    321  1.1  christos       run: |
    322  1.1  christos         # Verify the script generates the same output
    323  1.1  christos         ./scripts/gen_gh_actions.py > gh_actions_script.yml
    324  1.1  christos 
    325  1.1  christos         # Run autoconf
    326  1.1  christos         autoconf
    327  1.1  christos 
    328  1.1  christos         # Configure with flags
    329  1.1  christos         if [ -n "$COMPILER_FLAGS" ]; then
    330  1.1  christos           ./configure CC="${{CC}} ${{COMPILER_FLAGS}}" CXX="${{CXX}} ${{COMPILER_FLAGS}}" $CONFIGURE_FLAGS
    331  1.1  christos         else
    332  1.1  christos           ./configure $CONFIGURE_FLAGS
    333  1.1  christos         fi
    334  1.1  christos 
    335  1.1  christos         # Build
    336  1.1  christos         make -j3
    337  1.1  christos         make -j3 tests
    338  1.1  christos 
    339  1.1  christos         # Run tests
    340  1.1  christos         make check
    341  1.1  christos 
    342  1.1  christos """
    343  1.1  christos 
    344  1.1  christos     return job
    345  1.1  christos 
    346  1.1  christos 
    347  1.1  christos def generate_macos_job(arch):
    348  1.1  christos     """Generate macOS job configuration."""
    349  1.1  christos     os = OSX
    350  1.1  christos     max_unusual_opts = 1
    351  1.1  christos 
    352  1.1  christos     exclude = ([Option.as_malloc_conf(opt) for opt in (
    353  1.1  christos             'dss:primary',
    354  1.1  christos             'background_thread:true')] +
    355  1.1  christos         [Option.as_configure_flag('--enable-prof')] +
    356  1.1  christos         [CLANG,])
    357  1.1  christos 
    358  1.1  christos     if arch != ARM64:
    359  1.1  christos         exclude += [LARGE_HUGEPAGE]
    360  1.1  christos 
    361  1.1  christos     matrix_entries = generate_job_matrix_entries(os, arch, exclude, max_unusual_opts)
    362  1.1  christos 
    363  1.1  christos     arch_suffix = f"-{arch}" if arch != AMD64 else ""
    364  1.1  christos 
    365  1.1  christos     # Select appropriate runner based on architecture
    366  1.1  christos     # Pin both for more control over OS upgrades
    367  1.1  christos     if arch == ARM64:
    368  1.1  christos         runner = "macos-15"          # Pinned macOS 15 on Apple Silicon
    369  1.1  christos     else:  # AMD64
    370  1.1  christos         runner = "macos-15-intel"    # Pinned macOS 15 on Intel (last Intel runner, EOL Aug 2027)
    371  1.1  christos 
    372  1.1  christos     job = f"""  test-macos{arch_suffix}:
    373  1.1  christos     runs-on: {runner}
    374  1.1  christos     strategy:
    375  1.1  christos       fail-fast: false
    376  1.1  christos       matrix:
    377  1.1  christos         include:
    378  1.1  christos """
    379  1.1  christos 
    380  1.1  christos     for entry in matrix_entries:
    381  1.1  christos         job += "          - env:\n"
    382  1.1  christos         for key, value in entry.items():
    383  1.1  christos             if ' ' in str(value) or any(c in str(value) for c in [':', ',', '#']):
    384  1.1  christos                 job += f'              {key}: "{value}"\n'
    385  1.1  christos             else:
    386  1.1  christos                 job += f"              {key}: {value}\n"
    387  1.1  christos 
    388  1.1  christos     job += f"""
    389  1.1  christos     steps:
    390  1.1  christos     - uses: actions/checkout@v4
    391  1.1  christos 
    392  1.1  christos     - name: Show OS version
    393  1.1  christos       run: |
    394  1.1  christos         echo "=== macOS Version ==="
    395  1.1  christos         sw_vers
    396  1.1  christos         echo ""
    397  1.1  christos         echo "=== Architecture ==="
    398  1.1  christos         uname -m
    399  1.1  christos         arch
    400  1.1  christos         echo ""
    401  1.1  christos         echo "=== CPU Info ==="
    402  1.1  christos         sysctl -n machdep.cpu.brand_string
    403  1.1  christos         sysctl -n hw.machine
    404  1.1  christos 
    405  1.1  christos     - name: Install dependencies
    406  1.1  christos       run: |
    407  1.1  christos         brew install autoconf
    408  1.1  christos 
    409  1.1  christos     - name: Build and test
    410  1.1  christos       env:
    411  1.1  christos         CC: ${{{{ matrix.env.CC || 'gcc' }}}}
    412  1.1  christos         CXX: ${{{{ matrix.env.CXX || 'g++' }}}}
    413  1.1  christos         COMPILER_FLAGS: ${{{{ matrix.env.COMPILER_FLAGS }}}}
    414  1.1  christos         CONFIGURE_FLAGS: ${{{{ matrix.env.CONFIGURE_FLAGS }}}}
    415  1.1  christos         EXTRA_CFLAGS: ${{{{ matrix.env.EXTRA_CFLAGS }}}}
    416  1.1  christos       run: |
    417  1.1  christos         # Run autoconf
    418  1.1  christos         autoconf
    419  1.1  christos 
    420  1.1  christos         # Configure with flags
    421  1.1  christos         if [ -n "$COMPILER_FLAGS" ]; then
    422  1.1  christos           ./configure CC="${{CC}} ${{COMPILER_FLAGS}}" CXX="${{CXX}} ${{COMPILER_FLAGS}}" $CONFIGURE_FLAGS
    423  1.1  christos         else
    424  1.1  christos           ./configure $CONFIGURE_FLAGS
    425  1.1  christos         fi
    426  1.1  christos 
    427  1.1  christos         # Build
    428  1.1  christos         make -j3
    429  1.1  christos         make -j3 tests
    430  1.1  christos 
    431  1.1  christos         # Run tests
    432  1.1  christos         make check
    433  1.1  christos 
    434  1.1  christos """
    435  1.1  christos 
    436  1.1  christos     return job
    437  1.1  christos 
    438  1.1  christos 
    439  1.1  christos def generate_windows_job(arch):
    440  1.1  christos     """Generate Windows job configuration."""
    441  1.1  christos     os = WINDOWS
    442  1.1  christos     max_unusual_opts = 3
    443  1.1  christos     unusuals = (
    444  1.1  christos         Option.as_configure_flag('--enable-debug'),
    445  1.1  christos         CL,
    446  1.1  christos         CROSS_COMPILE_32BIT,
    447  1.1  christos     )
    448  1.1  christos 
    449  1.1  christos     matrix_entries = generate_job_matrix_entries(os, arch, (), max_unusual_opts, unusuals)
    450  1.1  christos 
    451  1.1  christos     arch_suffix = f"-{arch}" if arch != AMD64 else ""
    452  1.1  christos 
    453  1.1  christos     # Use latest for Windows - tends to be backward compatible and stable
    454  1.1  christos     job = f"""  test-windows{arch_suffix}:
    455  1.1  christos     runs-on: windows-latest
    456  1.1  christos     strategy:
    457  1.1  christos       fail-fast: false
    458  1.1  christos       matrix:
    459  1.1  christos         include:
    460  1.1  christos """
    461  1.1  christos 
    462  1.1  christos     for entry in matrix_entries:
    463  1.1  christos         job += "          - env:\n"
    464  1.1  christos         for key, value in entry.items():
    465  1.1  christos             if ' ' in str(value) or any(c in str(value) for c in [':', ',', '#']):
    466  1.1  christos                 job += f'              {key}: "{value}"\n'
    467  1.1  christos             else:
    468  1.1  christos                 job += f"              {key}: {value}\n"
    469  1.1  christos 
    470  1.1  christos     job += f"""
    471  1.1  christos     steps:
    472  1.1  christos     - uses: actions/checkout@v4
    473  1.1  christos 
    474  1.1  christos     - name: Show OS version
    475  1.1  christos       shell: cmd
    476  1.1  christos       run: |
    477  1.1  christos         echo === Windows Version ===
    478  1.1  christos         systeminfo | findstr /B /C:"OS Name" /C:"OS Version"
    479  1.1  christos         ver
    480  1.1  christos         echo.
    481  1.1  christos         echo === Architecture ===
    482  1.1  christos         echo PROCESSOR_ARCHITECTURE=%PROCESSOR_ARCHITECTURE%
    483  1.1  christos         echo.
    484  1.1  christos 
    485  1.1  christos     - name: Setup MSYS2
    486  1.1  christos       uses: msys2/setup-msys2@v2
    487  1.1  christos       with:
    488  1.1  christos         msystem: ${{{{ matrix.env.CROSS_COMPILE_32BIT == 'yes' && 'MINGW32' || 'MINGW64' }}}}
    489  1.1  christos         update: true
    490  1.1  christos         install: >-
    491  1.1  christos           autotools
    492  1.1  christos           git
    493  1.1  christos         pacboy: >-
    494  1.1  christos           make:p
    495  1.1  christos           gcc:p
    496  1.1  christos           binutils:p
    497  1.1  christos 
    498  1.1  christos     - name: Build and test (MinGW-GCC)
    499  1.1  christos       if: matrix.env.CC != 'cl.exe'
    500  1.1  christos       shell: msys2 {{0}}
    501  1.1  christos       env:
    502  1.1  christos         CC: ${{{{ matrix.env.CC || 'gcc' }}}}
    503  1.1  christos         CXX: ${{{{ matrix.env.CXX || 'g++' }}}}
    504  1.1  christos         COMPILER_FLAGS: ${{{{ matrix.env.COMPILER_FLAGS }}}}
    505  1.1  christos         CONFIGURE_FLAGS: ${{{{ matrix.env.CONFIGURE_FLAGS }}}}
    506  1.1  christos         EXTRA_CFLAGS: ${{{{ matrix.env.EXTRA_CFLAGS }}}}
    507  1.1  christos       run: |
    508  1.1  christos         # Run autoconf
    509  1.1  christos         autoconf
    510  1.1  christos 
    511  1.1  christos         # Configure with flags
    512  1.1  christos         if [ -n "$COMPILER_FLAGS" ]; then
    513  1.1  christos           ./configure CC="${{CC}} ${{COMPILER_FLAGS}}" CXX="${{CXX}} ${{COMPILER_FLAGS}}" $CONFIGURE_FLAGS
    514  1.1  christos         else
    515  1.1  christos           ./configure $CONFIGURE_FLAGS
    516  1.1  christos         fi
    517  1.1  christos 
    518  1.1  christos         # Build (mingw32-make is the "make" command in MSYS2)
    519  1.1  christos         mingw32-make -j3
    520  1.1  christos         mingw32-make tests
    521  1.1  christos 
    522  1.1  christos         # Run tests
    523  1.1  christos         mingw32-make -k check
    524  1.1  christos 
    525  1.1  christos     - name: Setup MSVC environment
    526  1.1  christos       if: matrix.env.CC == 'cl.exe'
    527  1.1  christos       uses: ilammy/msvc-dev-cmd@v1
    528  1.1  christos       with:
    529  1.1  christos         arch: ${{{{ matrix.env.CROSS_COMPILE_32BIT == 'yes' && 'x86' || 'x64' }}}}
    530  1.1  christos 
    531  1.1  christos     - name: Build and test (MSVC)
    532  1.1  christos       if: matrix.env.CC == 'cl.exe'
    533  1.1  christos       shell: msys2 {{0}}
    534  1.1  christos       env:
    535  1.1  christos         CONFIGURE_FLAGS: ${{{{ matrix.env.CONFIGURE_FLAGS }}}}
    536  1.1  christos         MSYS2_PATH_TYPE: inherit
    537  1.1  christos       run: |
    538  1.1  christos         # Export MSVC environment variables for configure
    539  1.1  christos         export CC=cl.exe
    540  1.1  christos         export CXX=cl.exe
    541  1.1  christos         export AR=lib.exe
    542  1.1  christos         export NM=dumpbin.exe
    543  1.1  christos         export RANLIB=:
    544  1.1  christos 
    545  1.1  christos         # Verify cl.exe is accessible (should be in PATH via inherit)
    546  1.1  christos         if ! which cl.exe > /dev/null 2>&1; then
    547  1.1  christos           echo "cl.exe not found, trying to locate MSVC..."
    548  1.1  christos           # Find and add MSVC bin directory to PATH
    549  1.1  christos           MSVC_BIN=$(cmd.exe /c "echo %VCToolsInstallDir%" | tr -d '\\\\r' | sed 's/\\\\\\\\\\\\\\\\/\\//g' | sed 's/C:/\\\\/c/g')
    550  1.1  christos           if [ -n "$MSVC_BIN" ]; then
    551  1.1  christos             export PATH="$PATH:$MSVC_BIN/bin/Hostx64/x64:$MSVC_BIN/bin/Hostx86/x86"
    552  1.1  christos           fi
    553  1.1  christos         fi
    554  1.1  christos 
    555  1.1  christos         # Run autoconf
    556  1.1  christos         autoconf
    557  1.1  christos 
    558  1.1  christos         # Configure with MSVC
    559  1.1  christos         ./configure CC=cl.exe CXX=cl.exe AR=lib.exe $CONFIGURE_FLAGS
    560  1.1  christos 
    561  1.1  christos         # Build (mingw32-make is the "make" command in MSYS2)
    562  1.1  christos         mingw32-make -j3
    563  1.1  christos         # Build tests sequentially due to PDB file issues
    564  1.1  christos         mingw32-make tests
    565  1.1  christos 
    566  1.1  christos         # Run tests
    567  1.1  christos         mingw32-make -k check
    568  1.1  christos 
    569  1.1  christos """
    570  1.1  christos 
    571  1.1  christos     return job
    572  1.1  christos 
    573  1.1  christos 
    574  1.1  christos def generate_freebsd_job(arch):
    575  1.1  christos     """Generate FreeBSD job configuration."""
    576  1.1  christos     # FreeBSD runs in a VM on ubuntu-latest, not native
    577  1.1  christos 
    578  1.1  christos     job = f"""  test-freebsd:
    579  1.1  christos     runs-on: ubuntu-latest
    580  1.1  christos     strategy:
    581  1.1  christos       fail-fast: false
    582  1.1  christos       matrix:
    583  1.1  christos         debug: ['--enable-debug', '--disable-debug']
    584  1.1  christos         prof: ['--enable-prof', '--disable-prof']
    585  1.1  christos         arch: ['64-bit', '32-bit']
    586  1.1  christos         uncommon:
    587  1.1  christos           - ''
    588  1.1  christos           - '--with-lg-page=16 --with-malloc-conf=tcache:false'
    589  1.1  christos 
    590  1.1  christos     name: FreeBSD (${{{{ matrix.arch }}}}, debug=${{{{ matrix.debug }}}}, prof=${{{{ matrix.prof }}}}${{{{ matrix.uncommon && ', uncommon' || '' }}}})
    591  1.1  christos 
    592  1.1  christos     steps:
    593  1.1  christos     - uses: actions/checkout@v4
    594  1.1  christos       with:
    595  1.1  christos         fetch-depth: 1
    596  1.1  christos 
    597  1.1  christos     - name: Test on FreeBSD
    598  1.1  christos       uses: vmactions/freebsd-vm@v1
    599  1.1  christos       with:
    600  1.1  christos         release: '15.0'
    601  1.1  christos         usesh: true
    602  1.1  christos         prepare: |
    603  1.1  christos           pkg install -y autoconf gmake
    604  1.1  christos         run: |
    605  1.1  christos           # Verify we're running in FreeBSD
    606  1.1  christos           echo "==== System Information ===="
    607  1.1  christos           uname -a
    608  1.1  christos           freebsd-version
    609  1.1  christos           echo "============================"
    610  1.1  christos 
    611  1.1  christos           # Set compiler flags for 32-bit if needed
    612  1.1  christos           if [ "${{{{ matrix.arch }}}}" = "32-bit" ]; then
    613  1.1  christos             export CC="cc -m32"
    614  1.1  christos             export CXX="c++ -m32"
    615  1.1  christos           fi
    616  1.1  christos 
    617  1.1  christos           # Generate configure script
    618  1.1  christos           autoconf
    619  1.1  christos 
    620  1.1  christos           # Configure with matrix options
    621  1.1  christos           ./configure --with-jemalloc-prefix=ci_ ${{{{ matrix.debug }}}} ${{{{ matrix.prof }}}} ${{{{ matrix.uncommon }}}}
    622  1.1  christos 
    623  1.1  christos           # Get CPU count for parallel builds
    624  1.1  christos           export JFLAG=$(sysctl -n kern.smp.cpus)
    625  1.1  christos 
    626  1.1  christos           gmake -j${{JFLAG}}
    627  1.1  christos           gmake -j${{JFLAG}} tests
    628  1.1  christos           gmake check
    629  1.1  christos 
    630  1.1  christos """
    631  1.1  christos 
    632  1.1  christos     return job
    633  1.1  christos 
    634  1.1  christos 
    635  1.1  christos def main():
    636  1.1  christos     import sys
    637  1.1  christos 
    638  1.1  christos     # Determine which workflow to generate based on command-line argument
    639  1.1  christos     workflow_type = sys.argv[1] if len(sys.argv) > 1 else 'linux'
    640  1.1  christos 
    641  1.1  christos     if workflow_type == 'linux':
    642  1.1  christos         jobs = '\n'.join((
    643  1.1  christos             generate_linux_job(AMD64),
    644  1.1  christos             generate_linux_job(ARM64),
    645  1.1  christos         ))
    646  1.1  christos         print(GITHUB_ACTIONS_TEMPLATE.format(name='Linux CI', jobs=jobs))
    647  1.1  christos 
    648  1.1  christos     elif workflow_type == 'macos':
    649  1.1  christos         jobs = '\n'.join((
    650  1.1  christos             generate_macos_job(AMD64),   # Intel x86_64
    651  1.1  christos             generate_macos_job(ARM64),   # Apple Silicon
    652  1.1  christos         ))
    653  1.1  christos         print(GITHUB_ACTIONS_TEMPLATE.format(name='macOS CI', jobs=jobs))
    654  1.1  christos 
    655  1.1  christos     elif workflow_type == 'windows':
    656  1.1  christos         jobs = generate_windows_job(AMD64)
    657  1.1  christos         print(GITHUB_ACTIONS_TEMPLATE.format(name='Windows CI', jobs=jobs))
    658  1.1  christos 
    659  1.1  christos     elif workflow_type == 'freebsd':
    660  1.1  christos         jobs = generate_freebsd_job(AMD64)
    661  1.1  christos         print(GITHUB_ACTIONS_TEMPLATE.format(name='FreeBSD CI', jobs=jobs))
    662  1.1  christos 
    663  1.1  christos     elif workflow_type == 'all':
    664  1.1  christos         # Generate all workflow files
    665  1.1  christos         linux_jobs = '\n'.join((
    666  1.1  christos             generate_linux_job(AMD64),
    667  1.1  christos             generate_linux_job(ARM64),
    668  1.1  christos         ))
    669  1.1  christos         macos_jobs = '\n'.join((
    670  1.1  christos             generate_macos_job(AMD64),   # Intel
    671  1.1  christos             generate_macos_job(ARM64),   # Apple Silicon
    672  1.1  christos         ))
    673  1.1  christos         windows_jobs = generate_windows_job(AMD64)
    674  1.1  christos         freebsd_jobs = generate_freebsd_job(AMD64)
    675  1.1  christos 
    676  1.1  christos         all_jobs = '\n'.join((linux_jobs, macos_jobs, windows_jobs, freebsd_jobs))
    677  1.1  christos         print(GITHUB_ACTIONS_TEMPLATE.format(name='CI', jobs=all_jobs))
    678  1.1  christos 
    679  1.1  christos     else:
    680  1.1  christos         print(f"Unknown workflow type: {workflow_type}", file=sys.stderr)
    681  1.1  christos         print("Usage: gen_gh_actions.py [linux|macos|windows|freebsd|all]", file=sys.stderr)
    682  1.1  christos         sys.exit(1)
    683  1.1  christos 
    684  1.1  christos 
    685  1.1  christos if __name__ == '__main__':
    686  1.1  christos     main()
    687