Skip to content

Commit de4765e

Browse files
authored
export command (#66)
* Basic functionality * Add comments to functions * Add option to create ocen debian packages * Add `dlazaw` package creation * Add only handwritten tests to exported package * Add package for testing `export` command * Fix shell ingen in test package * Add dlazaw directory in `lib` test package * Add new test package * Add unit tests * Small code fixes * Add tests for exporting archive * Add tests for exporting ocen package * Add tests for exporting dlazaw package * Move to setup.cfg * Update workflows * Fix tests on macOS * Fix formatting in setup.cfg * Remove ocen and dlazaw packages creation * Fix running export on packages without ingen * Update `export`'s description * Add `export` command informations to Readme * Move check if ingen_path is a shell script to the compile_ingen function * Refactor `copy_files` function's name * Copy all example tests * Rename `create_files` function * Fix attachments' directory name * Add function for exiting if not in package * Change --output flag description * Export package to the current directory * Print warnings about handwritten tests * Add maintainer * Use cache directory for exporting * Add missing mkdir * Update readme * Fix tests after removing --ouput flag * Bump version for release
1 parent 119a241 commit de4765e

27 files changed

+501
-79
lines changed

.github/workflows/Arch.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Prepare system
2121
run: |
2222
sysctl kernel.perf_event_paranoid=-1
23-
pacman -Syu --noconfirm diffutils time gcc
23+
pacman -Syu --noconfirm diffutils time gcc dpkg
2424
- name: Set up Python ${{ matrix.python-version }}
2525
uses: actions/setup-python@v4
2626
with:

.github/workflows/Ubuntu.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Prepare system
2323
run: |
2424
apt update
25-
apt install -y sqlite3 sqlite3-doc build-essential
25+
apt install -y sqlite3 sqlite3-doc build-essential dpkg
2626
sysctl kernel.perf_event_paranoid=-1
2727
- name: Set up Python ${{ matrix.python-version }}
2828
uses: actions/setup-python@v4

.github/workflows/macOS.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
pip3 install .[tests]
2424
- name: Install Homebrew dependencies
2525
run: |
26-
brew install gnu-time coreutils diffutils
26+
brew install gnu-time coreutils diffutils dpkg
2727
- name: Run pytest
2828
run: |
2929
python3 -m pytest -v --time-tool time

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Whenever the new input differs from the previous one, the model solution will be
6868
You can also specify your ingen source file which will be used. Run `sinol-make gen --help` to see available flags.
6969
- `sinol-make inwer` -- Verifies whether input files are correct using your "inwer.cpp" program. You can specify what inwer
7070
program to use, what tests to check and how many CPUs to use. Run `sinol-make inwer --help` to see available flags.
71+
- `sinol-make export` -- Creates archive ready to upload to sio2 or szkopul. Run `sinol-make export --help` to see all available flags.
7172

7273
### Reporting bugs and contributing code
7374

pyproject.toml

-49
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,3 @@
11
[build-system]
22
requires = ["setuptools>=67.0"]
33
build-backend = "setuptools.build_meta"
4-
5-
[project]
6-
name = "sinol-make"
7-
dynamic = ["version"]
8-
authors = [
9-
{ name="Mateusz Masiarz", email="[email protected]" }
10-
]
11-
maintainers = [
12-
{ name="Tomasz Nowak", email="[email protected]" }
13-
]
14-
description = "CLI tool for creating sio2 task packages"
15-
readme = "README.md"
16-
requires-python = ">=3.7"
17-
classifiers = [
18-
"Programming Language :: Python :: 3",
19-
"License :: OSI Approved :: MIT License",
20-
"Operating System :: OS Independent",
21-
]
22-
dependencies = [
23-
"argparse",
24-
"argcomplete",
25-
"requests",
26-
"PyYAML",
27-
"dictdiffer",
28-
"importlib-resources",
29-
]
30-
31-
[project.optional-dependencies]
32-
tests = [
33-
"pytest",
34-
"pytest-cov",
35-
"requests-mock",
36-
]
37-
38-
[project.urls]
39-
"Homepage" = "https://github.com/sio2project/sinol-make"
40-
"Bug Tracker" = "https://github.com/sio2project/sinol-make/issues"
41-
42-
[project.scripts]
43-
sinol-make = "sinol_make:main"
44-
45-
[tool.setuptools.dynamic]
46-
version = { attr = "sinol_make.__version__" }
47-
48-
[tool.pytest.ini_options]
49-
testpaths = ["tests"]
50-
markers = [
51-
"github_runner: Mark tests that require GitHub runner",
52-
]

setup.cfg

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[metadata]
2+
name = sinol_make
3+
version = attr: sinol_make.__version__
4+
author = Mateusz Masiarz
5+
author_email = [email protected]
6+
maintainer = Tomasz Nowak, Mateusz Masiarz
7+
8+
description = CLI tool for creating sio2 task packages
9+
long_description = file: README.md
10+
long_description_content_type = text/markdown
11+
project_urls =
12+
Bug Tracker = https://github.com/sio2project/sinol-make/issues
13+
Homepage = https://github.com/sio2project/sinol-make
14+
classifiers =
15+
Programming Language :: Python :: 3
16+
License :: OSI Approved :: MIT License
17+
Operating System :: OS Independent
18+
19+
[options]
20+
packages = find_namespace:
21+
packages_dir = src
22+
include_package_data = True
23+
python_requires = >=3.7
24+
install_requires =
25+
argparse
26+
argcomplete
27+
requests
28+
PyYAML
29+
dictdiffer
30+
importlib-resources
31+
32+
[options.packages.find]
33+
where = src
34+
35+
[options.extras_require]
36+
tests =
37+
pytest
38+
pytest-cov
39+
requests-mock
40+
41+
[options.entry_points]
42+
console_scripts =
43+
sinol-make = sinol_make:main
44+
45+
[tool:pytest]
46+
testpaths =
47+
tests
48+
markers =
49+
github_runner: Mark tests that require GitHub runner

src/sinol_make/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from sinol_make import util
88

9-
__version__ = "1.4.3"
9+
__version__ = "1.5.0"
1010

1111
def configure_parsers():
1212
parser = argparse.ArgumentParser(
+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import os
2+
import glob
3+
import shutil
4+
import tarfile
5+
import argparse
6+
import yaml
7+
8+
from sinol_make import util
9+
from sinol_make.helpers import package_util, parsers
10+
from sinol_make.commands.gen import gen_util
11+
from sinol_make.interfaces.BaseCommand import BaseCommand
12+
13+
14+
class Command(BaseCommand):
15+
"""
16+
Class for "export" command.
17+
"""
18+
19+
def get_name(self):
20+
return "export"
21+
22+
def configure_subparser(self, subparser: argparse.ArgumentParser):
23+
parser = subparser.add_parser(
24+
self.get_name(),
25+
help='Create archive for oioioi upload',
26+
description='Creates archive in the current directory ready to upload to sio2 or szkopul.')
27+
parsers.add_compilation_arguments(parser)
28+
29+
def get_generated_tests(self):
30+
"""
31+
Returns list of generated tests.
32+
Executes ingen to check what tests are generated.
33+
"""
34+
if not gen_util.ingen_exists(self.task_id):
35+
return []
36+
37+
working_dir = os.path.join(os.getcwd(), 'cache', 'export', 'tests')
38+
if os.path.exists(working_dir):
39+
shutil.rmtree(working_dir)
40+
os.makedirs(working_dir)
41+
42+
ingen_path = gen_util.get_ingen(self.task_id)
43+
ingen_exe = gen_util.compile_ingen(ingen_path, self.args, self.args.weak_compilation_flags)
44+
if not gen_util.run_ingen(ingen_exe, working_dir):
45+
util.exit_with_error('Failed to run ingen.')
46+
47+
tests = glob.glob(os.path.join(working_dir, f'{self.task_id}*.in'))
48+
return [package_util.extract_test_id(test) for test in tests]
49+
50+
def copy_package_required_files(self, target_dir: str):
51+
"""
52+
Copies package files and directories from
53+
current directory to target directory.
54+
:param target_dir: Directory to copy files to.
55+
"""
56+
files = ['config.yml', 'makefile.in', 'Makefile.in',
57+
'prog', 'doc', 'attachments', 'dlazaw']
58+
for file in files:
59+
file_path = os.path.join(os.getcwd(), file)
60+
if os.path.exists(file_path):
61+
if os.path.isdir(file_path):
62+
shutil.copytree(file_path, os.path.join(target_dir, file))
63+
else:
64+
shutil.copy(file_path, target_dir)
65+
66+
print('Copying example tests...')
67+
for ext in ['in', 'out']:
68+
os.mkdir(os.path.join(target_dir, ext))
69+
for test in glob.glob(os.path.join(os.getcwd(), ext, f'{self.task_id}0*.{ext}')):
70+
shutil.copy(test, os.path.join(target_dir, ext))
71+
72+
print('Generating tests...')
73+
generated_tests = self.get_generated_tests()
74+
tests_to_copy = []
75+
for ext in ['in', 'out']:
76+
for test in glob.glob(os.path.join(os.getcwd(), ext, f'{self.task_id}*.{ext}')):
77+
if package_util.extract_test_id(test) not in generated_tests:
78+
tests_to_copy.append(test)
79+
80+
if len(tests_to_copy) > 0:
81+
print(util.warning(f'Found {len(tests_to_copy)} tests that are not generated by ingen.'))
82+
for test in tests_to_copy:
83+
print(util.warning(f'Coping {os.path.basename(test)}...'))
84+
shutil.copy(test, os.path.join(target_dir, os.path.splitext(os.path.basename(test))[1]))
85+
86+
def create_makefile_in(self, target_dir: str, config: dict):
87+
"""
88+
Creates required `makefile.in` file.
89+
:param target_dir: Directory to create files in.
90+
:param config: Config dictionary.
91+
"""
92+
with open(os.path.join(target_dir, 'makefile.in'), 'w') as f:
93+
cxx_flags = '-std=c++17'
94+
c_flags = '-std=c17'
95+
if 'extra_compilation_args' in config:
96+
if 'cpp' in config['extra_compilation_args']:
97+
cxx_flags += ' ' + ' '.join(config['extra_compilation_args']['cpp'])
98+
if 'c' in config['extra_compilation_args']:
99+
c_flags += ' ' + ' '.join(config['extra_compilation_args']['c'])
100+
101+
f.write(f'MODE = wer\n'
102+
f'ID = {self.task_id}\n'
103+
f'SIG = sinolmake\n'
104+
f'\n'
105+
f'TIMELIMIT = {config["time_limit"]}\n'
106+
f'SLOW_TIMELIMIT = {4 * config["time_limit"]}\n'
107+
f'MEMLIMIT = {config["memory_limit"]}\n'
108+
f'\n'
109+
f'OI_TIME = oiejq\n'
110+
f'\n'
111+
f'CXXFLAGS += {cxx_flags}\n'
112+
f'CFLAGS += {c_flags}\n')
113+
114+
def compress(self, target_dir):
115+
"""
116+
Compresses target directory to archive.
117+
:param target_dir: Target directory path.
118+
:return: Path to archive.
119+
"""
120+
archive = os.path.join(os.getcwd(), f'{self.task_id}.tgz')
121+
with tarfile.open(archive, "w:gz") as tar:
122+
tar.add(target_dir, arcname=os.path.basename(target_dir))
123+
return archive
124+
125+
def run(self, args: argparse.Namespace):
126+
util.exit_if_not_package()
127+
128+
self.args = args
129+
self.task_id = package_util.get_task_id()
130+
131+
with open(os.path.join(os.getcwd(), 'config.yml'), 'r') as config_file:
132+
config = yaml.load(config_file, Loader=yaml.FullLoader)
133+
134+
export_package_path = os.path.join(os.getcwd(), 'cache', 'export', self.task_id)
135+
if os.path.exists(export_package_path):
136+
shutil.rmtree(export_package_path)
137+
os.makedirs(export_package_path)
138+
139+
self.copy_package_required_files(export_package_path)
140+
self.create_makefile_in(export_package_path, config)
141+
archive = self.compress(export_package_path)
142+
143+
print(util.info(f'Exported to {self.task_id}.tgz'))

src/sinol_make/commands/gen/__init__.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,15 @@ def calculate_md5_sums(self):
8888
return md5_sums, outputs_to_generate
8989

9090
def run(self, args: argparse.Namespace):
91-
if not util.check_if_project():
92-
util.exit_with_error('You are not in a project directory (couldn\'t find config.yml in current directory).')
91+
util.exit_if_not_package()
9392

9493
self.args = args
9594
self.task_id = package_util.get_task_id()
9695
self.ingen = gen_util.get_ingen(self.task_id, args.ingen_path)
9796
print(util.info(f'Using ingen file {os.path.basename(self.ingen)}'))
9897

9998
self.correct_solution = gen_util.get_correct_solution(self.task_id)
100-
101-
if os.path.splitext(self.ingen)[1] != '.sh':
102-
self.ingen_exe = gen_util.compile_ingen(self.ingen, self.args, self.args.weak_compilation_flags)
103-
else:
104-
self.ingen_exe = self.ingen
99+
self.ingen_exe = gen_util.compile_ingen(self.ingen, self.args, self.args.weak_compilation_flags)
105100

106101
if gen_util.run_ingen(self.ingen_exe):
107102
print(util.info('Successfully generated input files.'))

src/sinol_make/commands/gen/gen_util.py

+24-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
from sinol_make.helpers import compiler, package_util, compile
1111

1212

13+
def ingen_exists(task_id):
14+
"""
15+
Checks if ingen source file exists.
16+
:param task_id: task id, for example abc
17+
:return: True if exists, False otherwise
18+
"""
19+
return len(glob.glob(os.path.join(os.getcwd(), 'prog', task_id + 'ingen.*'))) > 0
20+
21+
1322
def get_ingen(task_id=None, ingen_path=None):
1423
"""
1524
Find ingen source file in `prog/` directory.
@@ -42,12 +51,18 @@ def get_ingen(task_id=None, ingen_path=None):
4251
def compile_ingen(ingen_path: str, args: argparse.Namespace, weak_compilation_flags=False):
4352
"""
4453
Compiles ingen and returns path to compiled executable.
54+
If ingen_path is shell script, then it will be returned.
4555
"""
56+
if os.path.splitext(ingen_path)[1] == '.sh':
57+
return ingen_path
58+
4659
compilers = compiler.verify_compilers(args, [ingen_path])
47-
ingen_exe, compile_log_path = compile.compile_file(ingen_path, package_util.get_executable(ingen_path), compilers, weak_compilation_flags)
60+
ingen_exe, compile_log_path = compile.compile_file(ingen_path, package_util.get_executable(ingen_path), compilers,
61+
weak_compilation_flags)
4862

4963
if ingen_exe is None:
50-
util.exit_with_error('Failed ingen compilation.', lambda: compile.print_compile_log(compile_log_path))
64+
compile.print_compile_log(compile_log_path)
65+
util.exit_with_error('Failed ingen compilation.')
5166
else:
5267
print(util.info('Successfully compiled ingen.'))
5368
return ingen_exe
@@ -81,21 +96,24 @@ def compile_correct_solution(solution_path: str, args: argparse.Namespace, weak_
8196
return correct_solution_exe
8297

8398

84-
85-
def run_ingen(ingen_exe):
99+
def run_ingen(ingen_exe, working_dir=None):
86100
"""
87101
Runs ingen and generates all input files.
88102
:param ingen_exe: path to ingen executable
103+
:param working_dir: working directory for ingen. If None, then {os.getcwd()}/in is used.
89104
:return: True if ingen was successful, False otherwise
90105
"""
106+
if working_dir is None:
107+
working_dir = os.path.join(os.getcwd(), 'in')
108+
91109
is_shell = os.path.splitext(ingen_exe)[1] == '.sh'
92110
if is_shell:
93-
util.fix_file_endings(ingen_exe)
111+
util.fix_line_endings(ingen_exe)
94112
st = os.stat(ingen_exe)
95113
os.chmod(ingen_exe, st.st_mode | stat.S_IEXEC)
96114

97115
process = subprocess.Popen([ingen_exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
98-
cwd=os.path.join(os.getcwd(), 'in'), shell=is_shell)
116+
cwd=working_dir, shell=is_shell)
99117
while process.poll() is None:
100118
print(process.stdout.readline().decode('utf-8'), end='')
101119

src/sinol_make/commands/inwer/__init__.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ def verify_and_print_table(self) -> Dict[str, TestResult]:
120120
return results
121121

122122
def run(self, args: argparse.Namespace):
123-
if not util.check_if_project():
124-
util.exit_with_error('You are not in a project directory (couldn\'t find config.yml in current directory).')
123+
util.exit_if_not_package()
125124

126125
self.task_id = package_util.get_task_id()
127126
self.inwer = inwer_util.get_inwer_path(self.task_id, args.inwer_path)

0 commit comments

Comments
 (0)