Update OCaml native arches for OCaml 5.1

Other changes:
- Remove the Python file and some macros (now in ocaml-rpm-macros)
- Add %ocaml_pkg macro for common declarations
This commit is contained in:
Jerry James 2023-10-04 20:48:27 -06:00
parent 178b940d71
commit c4baa36d2f
3 changed files with 43 additions and 506 deletions

View File

@ -4,10 +4,10 @@
# compilation is available on a particular architecture.
# Architectures that support the OCaml native code compiler.
%ocaml_native_compiler aarch64 x86_64
%ocaml_native_compiler aarch64 riscv64 s390x x86_64
# Architectures that support native dynamic linking of OCaml code.
%ocaml_natdynlink aarch64 x86_64
%ocaml_natdynlink aarch64 riscv64 s390x x86_64
# Architectures that support profiling of native code (ocamlopt -p).
# This was removed in OCaml 4.09.
@ -17,76 +17,39 @@
# Toplevel OCaml directory
%ocamldir %{_libdir}/ocaml
# This macro generates %package and %files definitions for a doc subpackage,
# Common elements for all OCaml packages.
#
# ExcludeArch: OCaml packages have not been built on i686 since OCaml 5 was
# introduced in Fedora 39.
# -d: most OCaml packages have no ELF objects when built on a bytecode-only
# architecture, so debuginfo generation is suppressed. Use this flag to
# enable a debuginfo package on such architectures; i.e., when the package
# contains an ELF object even on bytecode-only architectures.
%ocaml_pkg(d) %{lua:
print("ExcludeArch: %{ix86}\\n")
if not opt.d then
local arch = rpm.expand("%{_target_cpu}")
local native = rpm.expand("%{ocaml_native_compiler}")
if not string.find(native, arch) then
rpm.define('debug_package %{nil}')
end
end
}
# Generate %package and %files definitions for a doc subpackage,
# containing content generated by odoc.
# Use on the top-level only, preferably just before %prep.
#
# Use the -L option to specify the license file name. Example:
# %odoc_package -L LICENSE
%odoc_package(L:) \
%package doc \
BuildArch: noarch \
BuildRequires: ocaml-odoc \
Summary: Documentation for %{name} \
%description doc \
Developer documentation for %{name}. \
%files doc \
%doc _build/default/_doc/_html/* \
%{?-L:%%license %{-L*} %*}
# Add smp_mflags to arguments if no -j release option is given.
# Add --release to arguments if no -p or --release option is given.
# Add --verbose to arguments if it is not given.
%dune_add_flags(-) %{lua:
has_j = false
has_p = false
has_v = false
for _, flag in pairs(arg) do
if flag:find("^-j") then
has_j = true
elseif flag:find("^-p") or flag:find("^--release)") then
has_p = true
elseif flag:find("^--verbose") then
has_v = true
end
end
if not has_j then
table.insert(arg, 1, rpm.expand("%{?_smp_mflags}"))
end
if not has_p then
table.insert(arg, 1, "--release")
end
if not has_v then
table.insert(arg, 1, "--verbose")
end
print(table.concat(arg, " "))
}
# Build with dune
%dune_build(-) dune build %{dune_add_flags %*}
# Run tests with dune
%dune_check(-) dune runtest %{dune_add_flags %*}
# Make %files lists from an installed tree of files.
# The -s option enables separate packaging; every subdirectory of
# %{_libdir}/ocaml, except stublibs, is placed in its own package. This option
# requires the existence of opam *.install files in the build tree.
# The -n option suppresses creation of a devel subpackage.
# This macro requires that python3 be installed in the chroot.
%ocaml_files(sn) /usr/bin/python3 /usr/lib/rpm/redhat/ocaml_files.py %{-s} %{-n} %{buildroot} %{ocamldir}
# Install with dune
# The -s option enables separate packaging; every subdirectory of
# %{_libdir}/ocaml, except stublibs, is placed in its own package.
# The -n option suppresses creation of a devel subpackage.
# This macro requires that python3 be installed in the chroot.
%dune_install(sn) \
dune install --destdir=%{buildroot} %{dune_add_flags %*}; \
if [ -d _build/default/_doc/_html ]; then \
find _build/default/_doc/_html -name .dune-keep -delete; \
fi; \
rm -rf %{buildroot}%{_prefix}/doc; \
mlis=$(find %{buildroot}%{_libdir}/ocaml -name '*.mli'); \
rm -f ${mlis//.mli/.ml}; \
%ocaml_files %{-s} %{-n}
# Options:
# -L: specify the license file name. Example:
# %odoc_package -L LICENSE
%odoc_package(L:) %{expand:
%package doc
BuildArch: noarch
BuildRequires: ocaml-odoc
Summary: Documentation for %{name}
%description doc
Developer documentation for %{name}.
%files doc
%doc _build/default/_doc/_html/*
%{?-L:%%license %{-L*} %*}}

View File

@ -4,14 +4,13 @@
# dynamic linking.
#
# This package contains a file needed to define some RPM macros
# which are required before any SRPM is built, and a helper python
# script that executes common OCaml package tasks.
# which are required before any SRPM is built.
#
# See also: https://bugzilla.redhat.com/show_bug.cgi?id=1087794
Name: ocaml-srpm-macros
Version: 8
Release: 2%{?dist}
Version: 9
Release: 1%{?dist}
Summary: OCaml architecture macros
License: GPL-2.0-or-later
@ -19,7 +18,6 @@ License: GPL-2.0-or-later
BuildArch: noarch
Source0: macros.ocaml-srpm
Source1: ocaml_files.py
# NB. This package MUST NOT Require anything (except for dependencies
# that RPM itself generates).
@ -39,15 +37,17 @@ SRPMS. It does not pull in any other OCaml dependencies.
mkdir -p $RPM_BUILD_ROOT%{rpmmacrodir}
install -m 0644 %{SOURCE0} $RPM_BUILD_ROOT%{rpmmacrodir}/macros.ocaml-srpm
mkdir -p $RPM_BUILD_ROOT%{_rpmconfigdir}/redhat
install -m 0644 %{SOURCE1} $RPM_BUILD_ROOT%{_rpmconfigdir}/redhat
%files
%{rpmmacrodir}/macros.ocaml-srpm
%{_rpmconfigdir}/redhat/ocaml_files.py
%changelog
* Wed Oct 4 2023 Jerry James <loganjerry@gmail.com> - 9-1
- Update OCaml native arches for OCaml 5.1
- Remove the Python file and some macros (now in ocaml-rpm-macros)
- Add %%ocaml_pkg macro for common declarations
* Thu Jul 20 2023 Fedora Release Engineering <releng@fedoraproject.org> - 8-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild

View File

@ -1,426 +0,0 @@
# Copyright 2022, Jerry James
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the
# distribution.
# 3. Neither the name of Red Hat nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
import os
import string
import sys
from enum import Enum, auto
from typing import final, Tuple
# Version of this script
version=1
#
# BUILDROOT CATEGORIZATION
#
# Directories to ignore when generating %dir entries
root_dirs: set[str] = {
'/',
'/etc',
'/usr',
'/usr/bin',
'/usr/lib',
'/usr/lib/ocaml',
'/usr/lib/ocaml/caml',
'/usr/lib/ocaml/stublibs',
'/usr/lib/ocaml/threads',
'/usr/lib64',
'/usr/lib64/ocaml',
'/usr/lib64/ocaml/caml',
'/usr/lib64/ocaml/stublibs',
'/usr/lib64/ocaml/threads',
'/usr/libexec',
'/usr/sbin',
'/usr/share',
'/usr/share/doc'
}
def find_buildroot_toplevel(buildroot: str) -> list[str]:
"""Find toplevel files and directories in the buildroot.
:param str buildroot: path to the buildroot
:return: a list of toplevel files and directories in the buildroot
"""
bfiles: list[str] = []
for path, dirs, files in os.walk(buildroot):
for i in range(len(dirs) - 1, -1, -1):
d = os.path.join(path, dirs[i])[len(buildroot):]
if d not in root_dirs and not d.startswith('/usr/share/man'):
bfiles.append(d)
del dirs[i]
for f in files:
realfile = os.path.join(path, f)[len(buildroot):]
if realfile.startswith('/usr/share/man'):
bfiles.append(realfile + '*')
else:
bfiles.append(realfile)
return bfiles
# File suffixes that go into a devel subpackage
dev_suffixes: set[str] = {
'a', 'cmo', 'cmt', 'cmti', 'cmx', 'cmxa', 'h', 'idl', 'ml', 'mli', 'o'
}
def is_devel_file(filename: str) -> bool:
"""Determine whether a file belongs to a devel subpackage.
:param str filename: the filename to check
:return: True if the file belongs to a devel subpackage, else False
"""
return (filename == 'dune-package' or filename == 'opam' or
(os.path.splitext(filename)[1][1:] in dev_suffixes
and not filename.endswith('_top_init.ml')))
def find_buildroot_all(buildroot: str, devel: bool, add_star: bool) -> list[set[str]]:
"""Find all files and directories in the buildroot and optionally
categorize them as 'main' or 'devel'.
:param Namespace args: parsed command line arguments
:param bool devel: True to split into 'main' and 'devel', False otherwise
:param bool add_star: True to add a star to man page filenames
:return: a list of files and directories, in this order: main files,
main directories, devel files, and devel directories
"""
bfiles: list[set[str]] = [set(), set(), set()]
bdirs: set[str] = set()
for path, dirs, files in os.walk(buildroot):
for d in dirs:
realdir = os.path.join(path, d)[len(buildroot):]
if realdir not in root_dirs and not realdir.startswith('/usr/share/man'):
bdirs.add(realdir)
for f in files:
realfile = os.path.join(path, f)[len(buildroot):]
if devel and is_devel_file(os.path.basename(realfile)):
bfiles[2].add(realfile)
else:
if add_star and realfile.startswith('/usr/share/man'):
bfiles[0].add(realfile + '*')
else:
bfiles[0].add(realfile)
parentdir = os.path.dirname(realfile)
if parentdir in bdirs:
bfiles[1].add(parentdir)
bdirs.remove(parentdir)
bfiles.append(bdirs)
return bfiles
#
# INSTALL FILE LEXER AND PARSER
#
class TokenType(Enum):
"""The types of tokens that can appear in an opam *.install file."""
ERROR = auto()
EOF = auto()
COLON = auto()
LBRACE = auto()
RBRACE = auto()
LBRACK = auto()
RBRACK = auto()
STRING = auto()
FIELD = auto()
@final
class InstallFileLexer:
"""Convert an opam *.install file into a sequence of tokens."""
__slots__ = ['index', 'text']
def __init__(self) -> None:
"""Create an empty opam *.install file lexer."""
self.text = ''
self.index = 0
def lex_file(self, filename: str) -> None:
"""Prepare to read tokens from an opam *.install file.
:param str filename: the name of the file to read from
"""
with open(filename, 'r') as f:
# Limit reads to 4 MB in case this file is bogus.
# Most install files are under 4K.
self.text = f.read(4194304)
self.index = 0
def skip_whitespace(self) -> None:
"""Skip over whitespace in the input."""
while self.index < len(self.text) and \
(self.text[self.index] == '#' or
self.text[self.index] in string.whitespace):
if self.text[self.index] == '#':
while (self.index < len(self.text) and
self.text[self.index] != '\n' and
self.text[self.index] != '\r'):
self.index += 1
else:
self.index += 1
def token(self) -> Tuple[TokenType, str]:
"""Get the next token from the opam *.install file.
:return: a pair containing the type and text of the next token
"""
self.skip_whitespace()
if self.index < len(self.text):
ch = self.text[self.index]
if ch == ':':
self.index += 1
return (TokenType.COLON, ch)
if ch == '{':
self.index += 1
return (TokenType.LBRACE, ch)
if ch == '}':
self.index += 1
return (TokenType.RBRACE, ch)
if ch == '[':
self.index += 1
return (TokenType.LBRACK, ch)
if ch == ']':
self.index += 1
return (TokenType.RBRACK, ch)
if ch == '"':
start = self.index + 1
end = start
while end < len(self.text) and self.text[end] != '"':
end += 2 if self.text[end] == '\\' else 1
self.index = end + 1
return (TokenType.STRING, self.text[start:end])
if ch in string.ascii_letters:
start = self.index
end = start + 1
while (end < len(self.text) and
(self.text[end] == '_' or
self.text[end] in string.ascii_letters)):
end += 1
self.index = end
return (TokenType.FIELD, self.text[start:end])
return (TokenType.ERROR, ch)
else:
return (TokenType.EOF, '')
@final
class InstallFileParser:
"""Parse opam *.install files to generate RPM %files lists."""
__slots__ = ['buildroot', 'lexer', 'libdir', 'pkgs']
def __init__(self, buildroot: str, libdir: str, devel: bool) -> None:
"""Initialize an OCaml .install file parser.
:param str buildroot: path to the buildroot
:param str libdir: the OCaml library directory
:param bool devel: True to split into main and devel packages
"""
self.buildroot = find_buildroot_all(buildroot, devel, False)
self.libdir = libdir
self.lexer = InstallFileLexer()
self.pkgs: dict[str, set[str]] = dict()
def add_package(self, pkgname: str, filename: str) -> None:
"""Add a mapping from pkgname to filename.
:param str pkgname: the package that acts as the map key
:param str filename: the filename to add to the package set
"""
if pkgname not in self.pkgs:
self.pkgs[pkgname] = set()
self.pkgs[pkgname].add(filename)
def register_file(self, pkgname: str, filename: str) -> None:
"""Register one file listed in an opam *.install file.
:param str pkgname: name of the package to which this file belongs
:param str filename: name of the file
"""
if filename in self.buildroot[0]:
if filename.startswith('/usr/share/man'):
self.add_package(pkgname, filename + '*')
else:
self.add_package(pkgname, filename)
dirname = os.path.dirname(filename)
if dirname in self.buildroot[1]:
self.add_package(pkgname, '%dir ' + dirname)
self.buildroot[1].remove(dirname)
elif filename in self.buildroot[2]:
if filename.startswith('/usr/share/man'):
self.add_package(pkgname + '-devel', filename + '*')
else:
self.add_package(pkgname + '-devel', filename)
dirname = os.path.dirname(filename)
if dirname in self.buildroot[3]:
self.add_package(pkgname + '-devel', '%dir ' + dirname)
self.buildroot[3].remove(dirname)
def parse_file(self, filename: str) -> None:
"""Parse a .install file and add the contents to internal file lists.
If there are any parse errors, we assume this file is not really an
opam .install file and abandon the parse.
:param str filename: name of the .install file to parse
"""
# Get the package name from the filename
pkgname = os.path.splitext(os.path.basename(filename))[0]
# Map opam installer names to directories
opammap: dict[str, str] = {
'lib': os.path.join(self.libdir, pkgname),
'lib_root': self.libdir,
'libexec': os.path.join(self.libdir, pkgname),
'libexec_root': self.libdir,
'bin': '/usr/bin',
'sbin': '/usr/sbin',
'toplevel': os.path.join(self.libdir, 'toplevel'),
'share': os.path.join('/usr/share', pkgname),
'share_root': '/usr/share',
'etc': os.path.join('/etc', pkgname),
'doc': os.path.join('/usr/doc', pkgname),
'stublibs': os.path.join(self.libdir, 'stublibs'),
'man': '/usr/share/man'
}
# Prepare the lexer
self.lexer.lex_file(filename)
# Parse the file
toktyp, token = self.lexer.token()
while toktyp == TokenType.FIELD:
libname = token
toktyp, token = self.lexer.token()
if toktyp != TokenType.COLON:
return
toktyp, token = self.lexer.token()
if toktyp != TokenType.LBRACK:
return
directory = opammap.get(libname)
if not directory:
return
toktyp, token = self.lexer.token()
while toktyp == TokenType.STRING:
nexttp, nexttk = self.lexer.token()
if nexttp == TokenType.LBRACE:
nexttp, nexttk = self.lexer.token()
if nexttp == TokenType.STRING:
filnam = os.path.join(directory, nexttk)
bracetp, bractk = self.lexer.token()
if bracetp != TokenType.RBRACE:
return
nexttp, nexttk = self.lexer.token()
else:
return
elif libname == 'man':
index = token.rfind('.')
if index < 0:
return
mandir = os.path.join(directory, 'man' + token[index+1:])
filnam = os.path.join(mandir, os.path.basename(token))
else:
filnam = os.path.join(directory, os.path.basename(token))
toktyp, token = nexttp, nexttk
self.register_file(pkgname, filnam)
if toktyp != TokenType.RBRACK:
return
toktyp, token = self.lexer.token()
if toktyp != TokenType.EOF:
return
def parse_files(self) -> dict[str, set[str]]:
"""Find all .install files and parse each one.
For some projects, there are install files in both the project root
directory and somewhere under "_build", so be careful not to parse the
same install file twice.
:return: a map from package names to set of files to install
"""
install_files = set()
for path, dirs, files in os.walk('.'):
for f in files:
if f.endswith('.install') and f not in install_files:
install_files.add(f)
self.parse_file(os.path.join(path, f))
return self.pkgs
#
# MAIN INTERFACE
#
def ocaml_files(no_devel: bool, separate: bool, buildroot: str, libdir: str) -> None:
"""Generate %files lists from an installed buildroot.
:param bool no_devel: False to split files into a main package and a devel
package
:param bool separate: True to place each OCaml module in an RPM package
:param str buildroot: the installed buildroot
:param str libdir: the OCaml library directory
"""
if separate:
parser = InstallFileParser(buildroot, libdir, not no_devel)
pkgmap = parser.parse_files()
for pkg in pkgmap:
with open('.ofiles-' + pkg, 'w') as f:
for entry in pkgmap[pkg]:
f.write(entry + '\n')
elif no_devel:
with open('.ofiles', 'w') as f:
for entry in find_buildroot_toplevel(buildroot):
f.write(entry + '\n')
else:
files = find_buildroot_all(buildroot, True, True)
with open('.ofiles', 'w') as f:
for entry in files[0]:
f.write(entry + '\n')
for entry in files[1]:
f.write('%dir ' + entry + '\n')
with open('.ofiles-devel', 'w') as f:
for entry in files[2]:
f.write(entry + '\n')
for entry in files[3]:
f.write('%dir ' + entry + '\n')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Support for building OCaml RPM packages')
parser.add_argument('-n', '--no-devel',
action='store_true',
default=False,
help='suppress creation of a devel subpackage')
parser.add_argument('-s', '--separate',
action='store_true',
default=False,
help='separate packaging. Each OCaml module is in a distinct RPM package. All modules are in a single RPM package by default.')
parser.add_argument('-v', '--version',
action='version',
version=f'%(prog)s {str(version)}')
parser.add_argument('buildroot', help='RPM build root')
parser.add_argument('libdir', help='OCaml library directory')
args = parser.parse_args()
ocaml_files(args.no_devel, args.separate, args.buildroot, args.libdir)