397 lines
16 KiB
Python
Executable file
397 lines
16 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
# Copyright (C) 2023-2024 Free Software Foundation, Inc.
|
|
#
|
|
# Script to regenerate FOO.opt.urls files for each FOO.opt in the
|
|
# source tree.
|
|
#
|
|
# This file is part of GCC.
|
|
#
|
|
# GCC is free software; you can redistribute it and/or modify it under
|
|
# the terms of the GNU General Public License as published by the Free
|
|
# Software Foundation; either version 3, or (at your option) any later
|
|
# version.
|
|
#
|
|
# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
# for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with GCC; see the file COPYING3. If not see
|
|
# <http://www.gnu.org/licenses/>. */
|
|
|
|
DESCRIPTION = """
|
|
Parses the generated HTML (from "make html") to locate anchors
|
|
for options, then parses the .opt files within the source tree,
|
|
and generates a .opt.urls in the source tree for each .opt file,
|
|
giving URLs for each option, where it can.
|
|
|
|
Usage (from build/gcc subdirectory):
|
|
../../src/gcc/regenerate-opt-urls.py HTML/gcc-14.0.0/ ../../src
|
|
|
|
To run unit tests:
|
|
../../src/gcc/regenerate-opt-urls.py HTML/gcc-14.0.0/ ../../src --unit-test
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from pprint import pprint
|
|
import sys
|
|
import re
|
|
import unittest
|
|
|
|
def canonicalize_option_name(option_name):
|
|
if option_name.endswith('='):
|
|
option_name = option_name[0:-1]
|
|
return option_name
|
|
|
|
|
|
def canonicalize_url_suffix(url_suffix):
|
|
"""
|
|
Various options have anchors for both the positive and
|
|
negative form. For example -Wcpp has both:
|
|
'gcc/Warning-Options.html#index-Wno-cpp'
|
|
'gcc/Warning-Options.html#index-Wcpp'
|
|
|
|
Return a canonicalized version of the url_suffix that
|
|
strips out any "no-" prefixes, for use in deduplication.
|
|
Note that the resulting url suffix might not correspond to
|
|
an actual anchor in the HTML.
|
|
"""
|
|
url_suffix = re.sub('index-Wno-', 'index-W', url_suffix)
|
|
url_suffix = re.sub('index-fno-', 'index-f', url_suffix)
|
|
url_suffix = re.sub('_003d$', '', url_suffix)
|
|
url_suffix = re.sub('-([0-9]+)$', '', url_suffix)
|
|
return url_suffix
|
|
|
|
|
|
class Index:
|
|
def __init__(self):
|
|
# Map from language (or None) to map from option name to set of URL suffixes
|
|
self.entries = {}
|
|
|
|
def add_entry(self, matched_text, url_suffix, language, verbose=False):
|
|
if 'Attributes.html' in url_suffix:
|
|
return
|
|
matched_text = canonicalize_option_name(matched_text)
|
|
if language not in self.entries:
|
|
self.entries[language] = {}
|
|
per_lang_entries = self.entries[language]
|
|
if matched_text in per_lang_entries:
|
|
# Partition by canonicalized url_suffixes; add the
|
|
# first url_suffix in each such partition.
|
|
c_new = canonicalize_url_suffix(url_suffix)
|
|
for entry in per_lang_entries[matched_text]:
|
|
c_entry = canonicalize_url_suffix(entry)
|
|
if c_new == c_entry:
|
|
return
|
|
per_lang_entries[matched_text].add(url_suffix)
|
|
else:
|
|
per_lang_entries[matched_text] = set([url_suffix])
|
|
|
|
def get_languages(self):
|
|
return self.entries.keys()
|
|
|
|
def get_url_suffixes(self, text, language=None):
|
|
text = canonicalize_option_name(text)
|
|
per_lang_entries = self.entries.get(language)
|
|
if per_lang_entries:
|
|
return per_lang_entries.get(text)
|
|
|
|
def parse_option_index(self, input_filename, language, verbose=False):
|
|
with open(input_filename) as f:
|
|
dirname = input_filename.parent.name
|
|
for line in f:
|
|
self.parse_html_line_option_index(dirname, line, language, verbose)
|
|
|
|
def parse_html_line_option_index(self, dirname, line, language, verbose=False):
|
|
if verbose:
|
|
print(repr(line))
|
|
|
|
# Update for this in the GCC website's bin/preprocess process_html_file:
|
|
# | sed -e 's/_002d/-/g' -e 's/_002a/*/g' \
|
|
line = line.replace('_002d', '-')
|
|
line = line.replace('_002a', '*')
|
|
|
|
# e.g. <a href="Optimize-Options.html#index-fmodulo_002dsched"><code>fmodulo-sched</code></a>
|
|
m = re.search(r'<a href="([\S]+)"><code>([\S]+)</code></a>', line)
|
|
if not m:
|
|
return
|
|
if verbose:
|
|
print(m.groups())
|
|
url_suffix, index_text = m.groups()
|
|
option = '-' + index_text
|
|
|
|
# Strip off "no-" prefixes from options
|
|
if option[:5] == '-Wno-':
|
|
option = '-W' + option[5:]
|
|
if option[:5] == '-fno-':
|
|
option = '-f' + option[5:]
|
|
|
|
url_suffix = dirname + '/' + url_suffix
|
|
self.add_entry(option, url_suffix, language, verbose)
|
|
|
|
|
|
class TestParsingIndex(unittest.TestCase):
|
|
def test_parse_line(self):
|
|
index = Index()
|
|
index.parse_html_line_option_index('gcc',
|
|
'<a href="Optimize-Options.html#index-fmodulo_002dsched"><code>fmodulo-sched</code></a>',
|
|
None)
|
|
self.assertEqual(index.get_url_suffixes('-fmodulo-sched'),
|
|
{'gcc/Optimize-Options.html#index-fmodulo-sched'})
|
|
|
|
def test_negated_flag(self):
|
|
index = Index()
|
|
index.parse_html_line_option_index('gcc',
|
|
'<tr><td></td><td valign="top"><a href="Static-Analyzer-Options.html#index-fno_002danalyzer"><code>fno-analyzer</code></a>:</td><td> </td><td valign="top"><a href="Static-Analyzer-Options.html">Static Analyzer Options</a></td></tr>\n',
|
|
None)
|
|
self.assertEqual(index.get_url_suffixes('-fno-analyzer'), None)
|
|
self.assertEqual(index.get_url_suffixes('-fanalyzer'),
|
|
{'gcc/Static-Analyzer-Options.html#index-fno-analyzer'})
|
|
|
|
def test_negated_warning(self):
|
|
index = Index()
|
|
index.parse_html_line_option_index('gcc',
|
|
'<tr><td></td><td valign="top"><a href="Warning-Options.html#index-Wno_002dalloca"><code>Wno-alloca</code></a>:</td><td> </td><td valign="top"><a href="Warning-Options.html">Warning Options</a></td></tr>\n',
|
|
None)
|
|
self.assertEqual(index.get_url_suffixes('-Wno-alloca'),
|
|
None)
|
|
self.assertEqual(index.get_url_suffixes('-Walloca'),
|
|
{'gcc/Warning-Options.html#index-Wno-alloca'})
|
|
|
|
def test_parse_option_index(self):
|
|
index = Index()
|
|
index.parse_option_index(INPUT_HTML_PATH / 'gcc/Option-Index.html',
|
|
language=None)
|
|
self.assertEqual(index.get_url_suffixes('-fmodulo-sched'),
|
|
{'gcc/Optimize-Options.html#index-fmodulo-sched'})
|
|
self.assertEqual(index.get_url_suffixes('-O'),
|
|
{'gcc/Optimize-Options.html#index-O'})
|
|
self.assertEqual(index.get_url_suffixes('-O0'),
|
|
{'gcc/Optimize-Options.html#index-O0'})
|
|
self.assertEqual(index.get_url_suffixes('-Wframe-larger-than='),
|
|
{'gcc/Warning-Options.html#index-Wframe-larger-than_003d'})
|
|
|
|
# Check an option with duplicates: '-march'
|
|
# The url_suffixes will be of the form
|
|
# 'gcc/HPPA-Options.html#index-march-5',
|
|
# 'gcc/LoongArch-Options.html#index-march-7',
|
|
# etc, where the trailing number is, unfortunately, likely to
|
|
# change from release to release.
|
|
# Replace them with 'NN' for the purpose of this test:
|
|
em_arch_url_suffixes = [re.sub('(-[0-9]+)', '-NN', s)
|
|
for s in index.get_url_suffixes('-march')]
|
|
self.assertIn('gcc/ARM-Options.html#index-march-NN', em_arch_url_suffixes)
|
|
self.assertIn('gcc/x86-Options.html#index-march-NN', em_arch_url_suffixes)
|
|
|
|
self.assertEqual(index.get_url_suffixes('-Wcpp'),
|
|
{'gcc/Warning-Options.html#index-Wcpp'})
|
|
|
|
self.assertNotEqual(index.get_url_suffixes('-march'), None)
|
|
self.assertNotEqual(index.get_url_suffixes('-march='), None)
|
|
|
|
class OptFile:
|
|
def __init__(self, opt_path, rel_path):
|
|
"""
|
|
Parse a .opt file. Similar to opt-gather.awk.
|
|
"""
|
|
self.rel_path = rel_path
|
|
assert rel_path.startswith('gcc')
|
|
self.records = []
|
|
with open(opt_path) as f:
|
|
flag = 0
|
|
for line in f:
|
|
if re.match(r'[ \t]*(;|$)', line):
|
|
flag = 0
|
|
else:
|
|
if flag == 0:
|
|
self.records.append([line])
|
|
flag = 1
|
|
else:
|
|
self.records[-1].append(line)
|
|
|
|
# Mapping from target-specific page to subdirectory containing .opt files
|
|
# documented on that page.
|
|
|
|
TARGET_SPECIFIC_PAGES = {
|
|
'gcc/AArch64-Options.html' : 'gcc/config/aarch64/',
|
|
'gcc/AMD-GCN-Options.html' : 'gcc/config/gcn/',
|
|
'gcc/ARC-Options.html' : 'gcc/config/arc/',
|
|
'gcc/ARC-Options.html' : 'gcc/config/arc/',
|
|
'gcc/ARM-Options.html' : 'gcc/config/arm/',
|
|
'gcc/AVR-Options.html' : 'gcc/config/avr/',
|
|
'gcc/Adapteva-Epiphany-Options.html' : 'gcc/config/epiphany/',
|
|
'gcc/Blackfin-Options.html' : 'gcc/config/bfin/',
|
|
'gcc/C-SKY-Options.html' : 'gcc/config/csky/',
|
|
'gcc/C6X-Options.html' : 'gcc/config/c6x/',
|
|
'gcc/CRIS-Options.html' : 'gcc/config/cris/',
|
|
'gcc/DEC-Alpha-Options.html' : 'gcc/config/alpha/',
|
|
'gcc/FR30-Options.html' : 'gcc/config/fr30/',
|
|
'gcc/FRV-Options.html' : 'gcc/config/frv/',
|
|
'gcc/FT32-Options.html' : 'gcc/config/ft32/',
|
|
'gcc/H8_002f300-Options.html' : 'gcc/config/h8300/',
|
|
'gcc/HPPA-Options.html' : 'gcc/config/pa/',
|
|
'gcc/IA-64-Options.html' : 'gcc/config/ia64/',
|
|
'gcc/LoongArch-Options.html' : 'gcc/config/loongarch/',
|
|
'gcc/M32C-Options.html' : 'gcc/config/m32c/',
|
|
'gcc/M32R_002fD-Options.html' : 'gcc/config/m32r/',
|
|
'gcc/M680x0-Options.html' : 'gcc/config/m68k/',
|
|
'gcc/MCore-Options.html' : 'gcc/config/mcore/',
|
|
'gcc/MIPS-Options.html' : 'gcc/config/mips/',
|
|
'gcc/MMIX-Options.html' : 'gcc/config/mmix/',
|
|
'gcc/MN10300-Options.html' : 'gcc/config/mn10300/',
|
|
'gcc/MSP430-Options.html' : 'gcc/config/msp430/',
|
|
'gcc/MicroBlaze-Options.html' : 'gcc/config/microblaze/',
|
|
'gcc/Moxie-Options.html' : 'gcc/config/moxie/',
|
|
'gcc/NDS32-Options.html' : 'gcc/config/nds32/',
|
|
'gcc/Nios-II-Options.html' : 'gcc/config/nios2/',
|
|
'gcc/Nvidia-PTX-Options.html' : 'gcc/config/nvptx/',
|
|
'gcc/OpenRISC-Options.html' : 'gcc/config/or1k/',
|
|
'gcc/PDP-11-Options.html' : 'gcc/config/pdp11',
|
|
'gcc/PRU-Options.html' : 'gcc/config/pru/',
|
|
'gcc/RISC-V-Options.html' : 'gcc/config/riscv/',
|
|
'gcc/RL78-Options.html' : 'gcc/config/rl78/',
|
|
'gcc/RS_002f6000-and-PowerPC-Options.html' : 'gcc/config/rs6000/',
|
|
'gcc/RX-Options.html' : 'gcc/config/rx/',
|
|
'gcc/SH-Options.html' : 'gcc/config/sh/',
|
|
'gcc/SPARC-Options.html' : 'gcc/config/sparc/',
|
|
'gcc/S_002f390-and-zSeries-Options.html' : 'gcc/config/s390',
|
|
'gcc/V850-Options.html' : 'gcc/config/vax/',
|
|
'gcc/VAX-Options.html' : 'gcc/config/v850/',
|
|
'gcc/Visium-Options.html' : 'gcc/config/visium/',
|
|
'gcc/Xstormy16-Options.html' : 'gcc/config/stormy16/',
|
|
'gcc/Xtensa-Options.html' : 'gcc/config/xtensa/',
|
|
'gcc/eBPF-Options.html' : 'gcc/config/bpf/',
|
|
'gcc/x86-Options.html' : 'gcc/config/i386/',
|
|
}
|
|
|
|
def target_specific(url_suffix):
|
|
for page_prefix, subdir in TARGET_SPECIFIC_PAGES.items():
|
|
if url_suffix.startswith(page_prefix):
|
|
return subdir
|
|
|
|
def filter_urlsuffixes_for_optfile(optfile, url_suffixes):
|
|
"""
|
|
Filter out target-specific options for the wrong target.
|
|
"""
|
|
result = set()
|
|
for url_suffix in url_suffixes:
|
|
subdir = target_specific(url_suffix)
|
|
if subdir:
|
|
if not optfile.rel_path.startswith(subdir):
|
|
# Skip this
|
|
continue
|
|
result.add(url_suffix)
|
|
return result
|
|
|
|
|
|
class TestFiltering(unittest.TestCase):
|
|
def test_target_specific(self):
|
|
self.assertEqual(target_specific('gcc/Preprocessor-Options.html#index-A'),
|
|
None)
|
|
self.assertEqual(target_specific('gcc/MMIX-Options.html#index-mknuthdiv'),
|
|
'gcc/config/mmix/')
|
|
|
|
def test_filter(self):
|
|
s = {'gcc/MIPS-Options.html#index-munaligned-access-1',
|
|
'gcc/ARM-Options.html#index-munaligned-access'}
|
|
arm_optfile = OptFile('/dev/null', 'gcc/config/arm/arm.opt')
|
|
mips_optfile = OptFile('/dev/null', 'gcc/config/mips/mips.opt')
|
|
self.assertEqual(
|
|
filter_urlsuffixes_for_optfile(arm_optfile, s),
|
|
{'gcc/ARM-Options.html#index-munaligned-access'})
|
|
self.assertEqual(
|
|
filter_urlsuffixes_for_optfile(mips_optfile, s),
|
|
{'gcc/MIPS-Options.html#index-munaligned-access-1'})
|
|
|
|
|
|
def write_url_file(index, optfile, dstfile):
|
|
dstfile.write('; Autogenerated by regenerate-opt-urls.py from %s'
|
|
' and generated HTML\n\n'
|
|
% optfile.rel_path)
|
|
for record in optfile.records:
|
|
opt = '-' + record[0].strip()
|
|
url_suffixes_per_lang = {}
|
|
count = 0
|
|
for lang in index.get_languages():
|
|
this_lang_suffixes = index.get_url_suffixes(opt, language=lang)
|
|
url_suffixes_per_lang[lang] = this_lang_suffixes
|
|
if this_lang_suffixes:
|
|
count += len(this_lang_suffixes)
|
|
if not count:
|
|
continue
|
|
directives = []
|
|
for lang in index.get_languages():
|
|
if lang:
|
|
directive = 'LangUrlSuffix_%s for %r' % (lang, opt[1:])
|
|
else:
|
|
directive = 'UrlSuffix for %r' % opt[1:]
|
|
url_suffixes = url_suffixes_per_lang[lang]
|
|
if url_suffixes:
|
|
url_suffixes = filter_urlsuffixes_for_optfile(optfile, url_suffixes)
|
|
if url_suffixes:
|
|
if len(url_suffixes) == 1:
|
|
if lang:
|
|
directives.append('LangUrlSuffix_%s(%s)' % (lang, list(url_suffixes)[0]))
|
|
else:
|
|
directives.append('UrlSuffix(%s)' % list(url_suffixes)[0])
|
|
else:
|
|
dstfile.write('; skipping %s due to multiple URLs:\n'
|
|
% directive)
|
|
for u in sorted(url_suffixes):
|
|
dstfile.write('; duplicate: %r\n' % u)
|
|
else:
|
|
dstfile.write('; skipping %s due to finding no URLs\n'
|
|
% directive)
|
|
if directives:
|
|
dstfile.write('%s\n' % opt[1:])
|
|
dstfile.write(' '.join(directives) + '\n')
|
|
dstfile.write('\n')
|
|
|
|
# A list of (REL_PATH, LANG) pairs, where
|
|
# - REL_PATH is the relative path to a generated Option-Index.html file
|
|
# for a specific frontend, and
|
|
# - LANG is the name of the language (as specified in the "Language" record
|
|
# within the lang.opt file), or None for the language-independent
|
|
# documentation.
|
|
|
|
PER_LANGUAGE_OPTION_INDEXES = [
|
|
('gcc/Option-Index.html', None),
|
|
('gdc/Option-Index.html', 'D'),
|
|
('gfortran/Option-Index.html', 'Fortran')
|
|
]
|
|
|
|
def main(args):
|
|
index = Index()
|
|
for option_index_rel_path, lang in PER_LANGUAGE_OPTION_INDEXES:
|
|
index.parse_option_index(args.base_html_dir / option_index_rel_path,
|
|
language=lang)
|
|
for root, dirs, files in os.walk(args.src_gcc_dir):
|
|
for f in files:
|
|
if f.endswith('.opt'):
|
|
opt_path = os.path.join(root, f)
|
|
rel_path = os.path.relpath(opt_path, args.src_gcc_dir)
|
|
optfile = OptFile(opt_path, rel_path)
|
|
dstname = f + '.urls'
|
|
urlfile = os.path.join(root, dstname)
|
|
with open(urlfile, 'w') as dstfile:
|
|
write_url_file(index, optfile, dstfile)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description=DESCRIPTION,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument('base_html_dir', type=Path)
|
|
parser.add_argument('src_gcc_dir', type=Path)
|
|
parser.add_argument('--unit-test', action='store_true')
|
|
args = parser.parse_args()
|
|
|
|
if args.unit_test:
|
|
INPUT_HTML_PATH = args.base_html_dir
|
|
unittest.main(argv=[sys.argv[0], '-v'])
|
|
else:
|
|
main(args)
|