#!/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, '\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, 'contestACPC solutions\n')
preferences(home / 'contest/.settings/org.eclipse.core.resources.prefs', {'encoding/': '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', '')).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}')