297 lines
16 KiB
Python
297 lines
16 KiB
Python
#!/usr/bin/python3
|
|
"""Install contest compiler defaults for the installed IDEs and clean team homes."""
|
|
import configparser
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import pwd
|
|
import subprocess
|
|
import xml.etree.ElementTree as ET
|
|
|
|
changed = 0
|
|
JAVA_HOME = '/usr/lib/jvm/java-25-openjdk-amd64'
|
|
JAVA_OPTIONS = '-Dfile.encoding=UTF-8 -XX:+UseSerialGC -Xss64m -XX:TieredStopAtLevel=1 -XX:ActiveProcessorCount=2 -Xms2048m -Xmx2048m'
|
|
|
|
def write(path, text):
|
|
global changed
|
|
path = Path(path)
|
|
if not path.exists() or path.read_text() != text:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text)
|
|
changed += 1
|
|
|
|
def write_json(path, data):
|
|
write(path, json.dumps(data, indent=2) + '\n')
|
|
|
|
def read_json(path, default):
|
|
return json.loads(path.read_text()) if path.exists() else default
|
|
|
|
def geany(language, commands):
|
|
path = Path('/usr/share/geany/filedefs/filetypes.' + language)
|
|
conf = configparser.ConfigParser(interpolation=None, strict=False)
|
|
conf.optionxform = str
|
|
conf.read(path)
|
|
if not conf.has_section('build-menu'):
|
|
conf.add_section('build-menu')
|
|
for key, value in commands.items():
|
|
conf.set('build-menu', key, value)
|
|
stream = io.StringIO()
|
|
conf.write(stream, space_around_delimiters=False)
|
|
write(path, stream.getvalue())
|
|
|
|
for lang, cc in [('c', 'acpc-gcc'), ('cpp', 'acpc-g++')]:
|
|
geany(lang, {'FT_00_LB': '_Compile', 'FT_00_CM': cc + ' -c "%f" -o "%e.o"',
|
|
'FT_00_WD': '', 'FT_01_LB': '_Build', 'FT_01_CM': cc + ' "%f" -o "%e"',
|
|
'FT_01_WD': '', 'EX_00_LB': '_Execute', 'EX_00_CM': '"./%e"', 'EX_00_WD': ''})
|
|
geany('java', {'FT_00_LB': '_Compile', 'FT_00_CM': 'compilejava "%f"', 'FT_00_WD': '',
|
|
'FT_01_LB': '_Build', 'FT_01_CM': 'compilejava "%f"', 'FT_01_WD': '',
|
|
'EX_00_LB': '_Execute', 'EX_00_CM': 'runjava "%e"', 'EX_00_WD': ''})
|
|
geany('python', {'FT_00_LB': '_Compile', 'FT_00_CM': 'pypy3 -m py_compile "%f"', 'FT_00_WD': '',
|
|
'FT_01_LB': '_Build', 'FT_01_CM': 'pypy3 -m py_compile "%f"', 'FT_01_WD': '',
|
|
'EX_00_LB': '_Execute', 'EX_00_CM': 'pypy3 "%f"', 'EX_00_WD': ''})
|
|
|
|
def child(parent, tag):
|
|
node = parent.find(tag)
|
|
return node if node is not None else ET.SubElement(parent, tag)
|
|
|
|
def string(parent, tag, value):
|
|
child(child(parent, tag), 'str').text = value
|
|
|
|
def component(parent, name):
|
|
for node in parent.findall('component'):
|
|
if node.get('name') == name:
|
|
return node
|
|
return ET.SubElement(parent, 'component', name=name)
|
|
|
|
def option(parent, name, value):
|
|
node = next((n for n in parent.findall('option') if n.get('name') == name), None)
|
|
if node is None:
|
|
node = ET.SubElement(parent, 'option', name=name)
|
|
node.set('value', value)
|
|
|
|
def write_xml(path, root):
|
|
ET.indent(root)
|
|
write(path, ET.tostring(root, encoding='unicode') + '\n')
|
|
|
|
def preferences(path, values):
|
|
"""Update Java .properties keys, preserving unrelated workspace choices."""
|
|
data = {}
|
|
if path.exists():
|
|
for line in path.read_text().splitlines():
|
|
if '=' in line and not line.startswith('#'):
|
|
key, value = line.split('=', 1)
|
|
data[key] = value
|
|
for key, value in {'eclipse.preferences.version': '1', **values}.items():
|
|
data[key] = str(value).replace('\\', '\\\\').replace('\n', r'\n').replace('=', r'\=')
|
|
write(path, ''.join(k + '=' + v + '\n' for k, v in sorted(data.items())))
|
|
|
|
# Ask PyDev's own probe for the library paths of the installed PyPy.
|
|
probe = next(Path('/opt/eclipse/plugins').glob('org.python.pydev.core_*/pysrc/interpreterInfo.py'))
|
|
pyinfo = ET.fromstring(subprocess.check_output(['/usr/local/bin/pypy3', str(probe)], text=True))
|
|
ET.SubElement(pyinfo, 'name').text = 'ACPC PyPy 3.11'
|
|
ET.SubElement(pyinfo, 'vmArgs').text = ''
|
|
for lib in pyinfo.findall('lib'):
|
|
lib.attrib.clear()
|
|
pyinfo_text = ET.tostring(pyinfo, encoding='unicode') + '&&&&&'
|
|
|
|
for home in (Path('/home/team'), Path('/etc/skel')):
|
|
# Code::Blocks uses these settings for both single files and new projects.
|
|
cb_path = home / '.config/codeblocks/default.conf'
|
|
cb = ET.fromstring(cb_path.read_text()) if cb_path.exists() else ET.Element('CodeBlocksConfig', version='1')
|
|
compiler = child(cb, 'compiler')
|
|
string(compiler, 'SETTINGS_VERSION', '0.0.3')
|
|
string(compiler, 'DEFAULT_COMPILER', 'gcc')
|
|
gcc = child(child(compiler, 'sets'), 'gcc')
|
|
# Code::Blocks prepends MASTER_PATH/bin even to an absolute compiler name.
|
|
for key, value in {'NAME': 'ACPC GCC 15', 'MASTER_PATH': '/usr/local',
|
|
'EXTRA_PATHS': '/usr/bin',
|
|
'C_COMPILER': 'acpc-gcc', 'CPP_COMPILER': 'acpc-g++',
|
|
'LINKER': 'acpc-g++', 'MAKE': 'make', 'LIB_LINKER': 'ar'}.items():
|
|
string(gcc, key, value)
|
|
ET.indent(cb)
|
|
write(cb_path, '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(cb, encoding='unicode') + '\n')
|
|
|
|
code = home / '.config/Code/User'
|
|
settings = read_json(code / 'settings.json', {})
|
|
settings.update({
|
|
'python.defaultInterpreterPath': '/usr/local/bin/pypy3',
|
|
'C_Cpp.default.compilerPath': '/usr/bin/g++-15',
|
|
'C_Cpp.default.cStandard': 'c23', 'C_Cpp.default.cppStandard': 'c++23',
|
|
'java.jdt.ls.java.home': JAVA_HOME,
|
|
'java.configuration.runtimes': [{'name': 'JavaSE-25', 'path': JAVA_HOME, 'default': True}],
|
|
'java.debug.settings.vmArgs': JAVA_OPTIONS,
|
|
'debug.onTaskErrors': 'abort',
|
|
})
|
|
write_json(code / 'settings.json', settings)
|
|
tasks = read_json(code / 'tasks.json', {'version': '2.0.0', 'tasks': []})
|
|
tasks['tasks'] = [t for t in tasks['tasks'] if not t.get('label', '').startswith('ACPC:')]
|
|
for action in ('build', 'run'):
|
|
task = {'label': 'ACPC: ' + ('Build' if action == 'build' else 'Build and run'),
|
|
'type': 'process', 'command': '/usr/local/bin/acpc-solution',
|
|
'args': [action, '${file}'], 'options': {'cwd': '${fileDirname}'},
|
|
'problemMatcher': ['$gcc'],
|
|
'presentation': {'reveal': 'always', 'focus': True, 'panel': 'shared', 'clear': True}}
|
|
if action == 'build': task['group'] = {'kind': 'build', 'isDefault': True}
|
|
tasks['tasks'].append(task)
|
|
write_json(code / 'tasks.json', tasks)
|
|
bindings = read_json(code / 'keybindings.json', [])
|
|
bindings = [b for b in bindings if not (b.get('key') == 'f6' or str(b.get('args', '')).startswith('ACPC:'))]
|
|
bindings.append({'key': 'f6', 'command': 'workbench.action.tasks.runTask',
|
|
'args': 'ACPC: Build and run', 'when': 'editorTextFocus'})
|
|
write_json(code / 'keybindings.json', bindings)
|
|
(home / 'contest').mkdir(exist_ok=True)
|
|
project_path = home / 'contest/.project'
|
|
if not project_path.exists():
|
|
write(project_path, '<projectDescription><name>contest</name><comment>ACPC solutions</comment><projects/><buildSpec/><natures/></projectDescription>\n')
|
|
preferences(home / 'contest/.settings/org.eclipse.core.resources.prefs', {'encoding/<project>': 'UTF-8'})
|
|
|
|
workspace = home / 'eclipse-workspace'
|
|
prefs = workspace / '.metadata/.plugins/org.eclipse.core.runtime/.settings'
|
|
preferences(prefs / 'org.python.pydev.prefs', {
|
|
'INTERPRETER_PATH_NEW': pyinfo_text,
|
|
# PyDev's first-editor prompt would disable automatic Java/CDT builds
|
|
# across this mixed-language workspace. Keep those defaults and skip it.
|
|
'CHECK_PREFERRED_PYDEV_SETTINGS': 'false',
|
|
})
|
|
preferences(prefs / 'org.eclipse.jdt.core.prefs', {
|
|
'org.eclipse.jdt.core.compiler.compliance': '25',
|
|
'org.eclipse.jdt.core.compiler.source': '25',
|
|
'org.eclipse.jdt.core.compiler.codegen.targetPlatform': '25',
|
|
})
|
|
vm_type = 'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType'
|
|
vm_id = 'acpc-java-25'
|
|
vms = ET.Element('vmSettings', defaultVM=f'{len(vm_type)},{vm_type}{len(vm_id)},{vm_id}', defaultVMConnector='')
|
|
vmtype = ET.SubElement(vms, 'vmType', id=vm_type)
|
|
ET.SubElement(vmtype, 'vm', id=vm_id, name='ACPC Java 25', path=JAVA_HOME, vmArgs=JAVA_OPTIONS)
|
|
preferences(prefs / 'org.eclipse.jdt.launching.prefs', {'org.eclipse.jdt.launching.PREF_VM_XML': ET.tostring(vms, encoding='unicode')})
|
|
preferences(prefs / 'org.eclipse.ui.prefs', {'showIntro': 'false'})
|
|
preferences(prefs / 'org.eclipse.core.resources.prefs', {'encoding': 'UTF-8'})
|
|
for action in ('build', 'run'):
|
|
title = 'ACPC ' + ('Build' if action == 'build' else 'Build and run')
|
|
launch = ET.Element('launchConfiguration', type='org.eclipse.ui.externaltools.ProgramLaunchConfigurationType')
|
|
for key, value in {
|
|
'org.eclipse.ui.externaltools.ATTR_LOCATION': '/usr/local/bin/acpc-solution',
|
|
'org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS': action + ' "${resource_loc}"',
|
|
'org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY': '${container_loc}',
|
|
'org.eclipse.debug.core.ATTR_REFRESH_SCOPE': '${workspace}',
|
|
}.items():
|
|
ET.SubElement(launch, 'stringAttribute', key=key, value=value)
|
|
for key, value in {
|
|
'org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND': 'true',
|
|
'org.eclipse.debug.core.ATTR_CAPTURE_OUTPUT': 'true',
|
|
'org.eclipse.debug.ui.ATTR_CONSOLE_OUTPUT_ON': 'true',
|
|
'org.eclipse.debug.ui.ATTR_CONSOLE_ERROR_ON': 'true',
|
|
}.items():
|
|
ET.SubElement(launch, 'booleanAttribute', key=key, value=value)
|
|
favorites = ET.SubElement(launch, 'listAttribute', key='org.eclipse.debug.ui.favoriteGroups')
|
|
ET.SubElement(favorites, 'listEntry', value='org.eclipse.ui.externaltools.launchGroup')
|
|
write_xml(workspace / '.metadata/.plugins/org.eclipse.debug.core/.launches' / (title + '.launch'), launch)
|
|
history_path = workspace / '.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml'
|
|
history = ET.fromstring(history_path.read_text()) if history_path.exists() else ET.Element('launchHistory')
|
|
group_id = 'org.eclipse.ui.externaltools.launchGroup'
|
|
group = next((g for g in history if g.get('id') == group_id), None)
|
|
if group is None:
|
|
group = ET.SubElement(history, 'launchGroup', id=group_id)
|
|
child(group, 'mruHistory')
|
|
favorites = child(group, 'favorites')
|
|
for title in ('ACPC Build and run', 'ACPC Build'):
|
|
if not any(title == ET.fromstring(n.get('memento', '<x/>')).get('path') for n in favorites):
|
|
ET.SubElement(favorites, 'launch', memento=ET.tostring(ET.Element('launchConfiguration', local='true', path=title), encoding='unicode'))
|
|
write_xml(history_path, history)
|
|
|
|
for ide in ('clion', 'intellij-idea', 'pycharm'):
|
|
info = Path('/opt') / ide / 'product-info.json'
|
|
if not info.exists():
|
|
continue
|
|
options = home / '.config/JetBrains' / json.loads(info.read_text())['dataDirectoryName'] / 'options'
|
|
default_file = options / 'project.default.xml'
|
|
defaults = ET.fromstring(default_file.read_text()) if default_file.exists() else ET.Element('application')
|
|
project = child(component(defaults, 'ProjectManager'), 'defaultProject')
|
|
runs = component(project, 'RunManager')
|
|
if ide in ('pycharm', 'clion'):
|
|
sdk_file = options / 'jdk.table.xml'
|
|
sdks = ET.fromstring(sdk_file.read_text()) if sdk_file.exists() else ET.Element('application')
|
|
table = component(sdks, 'ProjectJdkTable')
|
|
sdk_name = 'PyPy 3.11 (as on Kattis)'
|
|
for node in list(table):
|
|
if node.find('name') is not None and node.find('name').get('value') == sdk_name:
|
|
table.remove(node)
|
|
sdk = ET.SubElement(table, 'jdk', version='2')
|
|
for key, value in [('name', sdk_name), ('type', 'Python SDK'),
|
|
('version', 'Python 3.11.15'), ('homePath', '/usr/local/bin/pypy3')]:
|
|
ET.SubElement(sdk, key, value=value)
|
|
roots = ET.SubElement(sdk, 'roots')
|
|
cp = ET.SubElement(ET.SubElement(roots, 'classPath'), 'root', type='composite')
|
|
for lib in pyinfo.findall('lib'):
|
|
ET.SubElement(cp, 'root', url='file://' + lib.text, type='simple')
|
|
ET.SubElement(ET.SubElement(roots, 'sourcePath'), 'root', type='composite')
|
|
additional = ET.SubElement(sdk, 'additional', SDK_UUID='3f7d6c1e-2a4b-4c8d-9e0f-a1b2c3d4e5f6')
|
|
ET.SubElement(additional, 'setting', name='FLAVOR_ID', value='PyPySdkFlavor')
|
|
ET.SubElement(additional, 'setting', name='FLAVOR_DATA', value='{}')
|
|
write_xml(sdk_file, sdks)
|
|
root_manager = component(project, 'ProjectRootManager')
|
|
root_manager.attrib.update({'version': '2', 'project-jdk-name': sdk_name, 'project-jdk-type': 'Python SDK'})
|
|
if ide == 'clion':
|
|
chain_file = options / 'linux/toolchains.xml'
|
|
chains = ET.fromstring(chain_file.read_text()) if chain_file.exists() else ET.Element('application')
|
|
cpp = component(chains, 'CPPToolchains')
|
|
cpp.set('version', '10')
|
|
entries = child(cpp, 'toolchains')
|
|
entries.set('detectedVersion', '5')
|
|
chain = entries.find('toolchain')
|
|
if chain is None:
|
|
chain = ET.SubElement(entries, 'toolchain', name='Default', toolSetKind='SYSTEM_UNIX_TOOLSET', debuggerKind='BUNDLED_GDB')
|
|
chain.set('customCCompilerPath', '/usr/local/bin/acpc-gcc')
|
|
chain.set('customCXXCompilerPath', '/usr/local/bin/acpc-g++')
|
|
write_xml(chain_file, chains)
|
|
for node in list(runs):
|
|
if node.get('default') == 'true' and node.get('type') == 'CppFileRunConfiguration':
|
|
runs.remove(node)
|
|
cfg = ET.SubElement(runs, 'configuration', default='true', type='CppFileRunConfiguration', factoryName='CppFileRunConfiguration')
|
|
option(cfg, 'compilerFile', '/usr/local/bin/acpc-cc')
|
|
option(cfg, 'compilerOptions', '')
|
|
if ide == 'intellij-idea':
|
|
sdk_file = options / 'jdk.table.xml'
|
|
sdks = ET.fromstring(sdk_file.read_text()) if sdk_file.exists() else ET.Element('application')
|
|
table = component(sdks, 'ProjectJdkTable')
|
|
for node in list(table):
|
|
if node.find('name') is not None and node.find('name').get('value') == 'ACPC Java 25':
|
|
table.remove(node)
|
|
sdk = ET.SubElement(table, 'jdk', version='2')
|
|
for key, value in [('name', 'ACPC Java 25'), ('type', 'JavaSDK'),
|
|
('version', 'java version "25.0.4.1"'), ('homePath', JAVA_HOME)]:
|
|
ET.SubElement(sdk, key, value=value)
|
|
roots = ET.SubElement(sdk, 'roots')
|
|
cp = ET.SubElement(ET.SubElement(roots, 'classPath'), 'root', type='composite')
|
|
for module in sorted(Path(JAVA_HOME, 'jmods').glob('*.jmod')):
|
|
ET.SubElement(cp, 'root', url=f'jrt://{JAVA_HOME}!/{module.stem}', type='simple')
|
|
ET.SubElement(ET.SubElement(roots, 'sourcePath'), 'root', type='composite')
|
|
ET.SubElement(sdk, 'additional')
|
|
write_xml(sdk_file, sdks)
|
|
root_manager = component(project, 'ProjectRootManager')
|
|
root_manager.attrib.update({'version': '2', 'languageLevel': 'JDK_25',
|
|
'project-jdk-name': 'ACPC Java 25', 'project-jdk-type': 'JavaSDK'})
|
|
for kind, factory in [('Application', 'Application'), ('JavaScratch', 'Java Scratch')]:
|
|
for node in list(runs):
|
|
if node.get('default') == 'true' and node.get('type') == kind:
|
|
runs.remove(node)
|
|
cfg = ET.SubElement(runs, 'configuration', default='true', type=kind, factoryName=factory)
|
|
option(cfg, 'VM_PARAMETERS', JAVA_OPTIONS)
|
|
option(cfg, 'ALTERNATIVE_JRE_PATH', JAVA_HOME)
|
|
option(cfg, 'ALTERNATIVE_JRE_PATH_ENABLED', 'true')
|
|
encoding = component(project, 'Encoding')
|
|
for node in list(encoding):
|
|
if node.get('url') == 'PROJECT':
|
|
encoding.remove(node)
|
|
ET.SubElement(encoding, 'file', url='PROJECT', charset='UTF-8')
|
|
write_xml(default_file, defaults)
|
|
|
|
if home == Path('/home/team'):
|
|
account = pwd.getpwnam('team')
|
|
for folder in (home / '.config/codeblocks', code, home / 'contest', home / '.config/JetBrains', workspace):
|
|
for path in [folder, *folder.rglob('*')]:
|
|
os.chown(path, account.pw_uid, account.pw_gid)
|
|
|
|
print(f'changed={changed}')
|