1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
|
#!/usr/bin/python3
"""
Module file handling for Fortran
Copyright (C) 2025 Alastair McKinstry <mckinstry@debian.org>
Released under the GPL-3 Gnu Public License.
"""
import click
import magic
import os
import dhfortran.debhelper as dh
import dhfortran.cli as cli
import dhfortran.compilers as cmplrs
import gzip
def which_compiler_flavor_and_mod_version(modfile: str):
"""For a modfile (path), return a tuple of strings for the compiler and modversion"""
compiler = ""
mod_version = ""
# Helper
def get_first_line(modfile):
try:
magick = magic.from_file(modfile)
except Exception as ex:
raise Exception(f"Can't open modfile {modfile}: {ex}")
if magick.startswith("gzip compressed"):
with gzip.open(modfile, "rt") as f:
return f.readline()
# TODO: Other case(s)
match get_first_line(modfile):
case line if line.startswith("GFORTRAN"):
mod_version = line.split()[3][1:-1]
if line.find("created from flang") != -1:
compiler = "flangext"
else:
compiler = "gfortran"
case line if line.startswith("LCompilers Modfile"):
version = line[18:] # eg '0.45.0'
compiler, mod_version = "lfortran", "0"
case line if line.startswith("V35"):
compiler, mod_version = "nvhpc", "35"
case line if line.startswith("V"):
# could also be PGI, but we don't ship PGI on Debian ... its an old flang
compiler = "flang"
mod_version = line.split()[0][1:]
case line if line.startswith("!mod$"):
compiler = ("flang",)
mod_version = line.split()[1][1:]
case _:
compiler, mod_version = "unknown", "0"
# mime = magick.split(";")[0]
# if (mime == "test/plain") or (mime == "text/x-c++"):
# with open(modfile) as mf:
# strings = mf.readstrings()
# else:
# if mime == "application/gzip":
# pass
return (compiler, mod_version)
##
## Modfile-specific interface for debhelper
##
class ModFileHelper(dh.DhFortranHelper):
def __init__(self, options):
super().__init__(options, "fortran-mod", "dh_fortran_mod")
# For tracking which compilers we need
self.modversions = {}
def compute_dest(self, modfile, target_dest=None):
"""Where does modfile go ?"""
# cli.verbose_print("compute_dest [mod] {modfile} {target_dest}")
# Should be called by base DebHelper() to move files
cmplr, version = which_compiler_flavor_and_mod_version(modfile)
fmoddir = f"/usr/lib/{cmplrs.multiarch}/fortran/{cmplr}-mod-{version}"
if target_dest is None:
return fmoddir
if target_dest.startswith("/"):
return target_dest
else:
return f"{fmoddir}/{target_dest}"
def process_file(self, pkg, oldfile, target_pkg, target_dest=None):
"""Called by DebHelper
Module file gets copied; metadata gets stored"""
cli.verbose_print(
f"DEBUG: process_file [module] name {oldfile} target_pkg {target_pkg} dest {target_dest}"
)
compiler, version = which_compiler_flavor_and_mod_version(oldfile)
if pkg not in self.modversions:
self.modversions[pkg] = set()
self.modversions[pkg].add(f"{compiler}-mod-{version}")
dest = self.compute_dest(oldfile, target_dest)
self.install_dir(f"{target_pkg}/{dest}")
self.doit(["cp", oldfile, f"{target_pkg}/{dest}"])
@click.command(
context_settings=dict(
ignore_unknown_options=True,
)
)
@click.argument("files", nargs=-1, type=click.UNPROCESSED)
@cli.debhelper_common_args
def dh_fortran_mod(files, *args, **kwargs):
"""
B<dh_fortran_mod> is a debhelper program that finds Fortran module and submodule files and
adds dependencies to B<gfortran-$version> as required to the package using
via the variable B<${fortran:Depends}>.
B<dh_fortran_mod> is expected to be automatically added using the debhelper "addon" B<fortran_mod>
ie. either automatically, by build-depending on 'dh-sequence-fortran-mod', or explicitly:
dh $@ --with fortran
B<dh_fortran_mod>Searches the debian/ directory for files B<debian/pkg.fortran-mod>$ which list
module files to include, with the same syntax as debhelper install files.
=head1 OPTIONS
=over 4
=item B<--sourcedir=>I<dir>
Look in the specified directory for files to be installed.
Typically Fortran module files are included in library development packages.
=back
=head1 TODO
Add dh-fortran-mod support for generic fortran compilers (ifx, etc).
=over 4
B<dh_fortran_mod> will be expanded to find mod files automatically from the I<debian/tmp> directory.
It will enable the installation of mod files in parallel for multiple compilers.
It will install .smod files for Fortran 2018.
The fortran-mod file syntax follows dh_install: pairs of sources and optional target directories.
The default directory will be $fmoddir ( /usr/lib/$multiarch/fortran/$compiler_mod_directory/)
If the target directory is absolute (starts with a '/'), this directory is used in the target package.
If the target does not absolute, it will be treated as a subdirectory of $fmoddir.
Currently four flavours of Fortran compiler are supported: gfortran-* ('gfortran'),
flang-new-* ('flang') , flang-to-external-fc-* ('flangext') and lfortran ('lfortran').
$compiler_mod_directory is based on the compiler module version: currently gfortran-mod-15 for
gfortran-14 (and older), flang-mod-15 for flang-new-15+ and lfortran-mod-0 for lfortran.
For flang-to-external-fc-* the version is flangext-mod-15 (assuming gfortran-14 as the external compiler);
in principle flang-to-external-fc ('flangext' flavour) and gfortran are intercompatible but
intermixing is avoided.
These will be updated for incompatible compiler versions.
Support for Makefiles and debian/rules is given in /usr/share/debhelper/dh-fortran/fortran-support.mk
This enables, for example:
/usr/share/debhelper/dh-fortran/fortran-support.mk
FMODDIR:= $(call get_fmoddir, gfortran)
FC_EXE:= $(call get_fc_exe, $(FC_DEFAULT))
"""
cli.verbose_print(f"dh_fortran_mod called with files {files} kwargs {kwargs}")
d = ModFileHelper(dh.build_options(**kwargs))
d.process_and_move_files(*files)
depends = []
for package in d.packages:
if package in d.modversions:
for m in d.modversions[package]:
comps = set()
for c in cmplrs.compilers:
if cmplrs.compilers[c]["mod"] == m:
comps.add(c)
depends.append("|".join(comps) + " | " + m)
d.addsubstvar(package, "fortran:Depends", ",".join(depends))
if d.packages[package]["config_file"] and package in d.modversions:
# config file defined, so create cleanup script
for modversion in d.modversions[package]:
d.autoscript(
package,
"postrm",
"postrm-fortran-mod",
{
"MULTIARCH": cmplrs.multiarch,
"FCOMPILERMOD": modversion,
},
)
d.save_substvars()
if __name__ == "__main__":
import pytest
pytest.main(["tests/module.py"])
|