2021-02-13 15:51:52 +03:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import os
|
|
|
|
import re
|
2021-04-02 12:26:37 +03:00
|
|
|
import sys
|
2021-04-23 14:53:53 +03:00
|
|
|
import subprocess
|
2022-01-08 14:27:38 +03:00
|
|
|
from pathlib import Path
|
|
|
|
from tempfile import NamedTemporaryFile
|
2021-02-13 15:51:52 +03:00
|
|
|
|
2021-04-23 14:53:53 +03:00
|
|
|
# Matches e.g. "| [`bash`](bash/) | GNU Bash | 5.0 | https://www.gnu.org/software/bash/ |"
|
|
|
|
# and captures "bash" in group 1, "bash/" in group 2, "<spaces>" in group 3, "GNU Bash" in group 4, "5.0" in group 5
|
|
|
|
# and "https://www.gnu.org/software/bash/" in group 6.
|
|
|
|
PORT_TABLE_REGEX = re.compile(
|
|
|
|
r'^\| \[`([^`]+)`\]\(([^\)]+)\)([^\|]+) \| ([^\|]+) \| ([^\|]+?) \| ([^\|]+) \|+$', re.MULTILINE
|
|
|
|
)
|
|
|
|
|
|
|
|
# Matches non-abbreviated git hashes
|
|
|
|
GIT_HASH_REGEX = re.compile(r'^[0-9a-f]{40}$')
|
2021-02-13 15:51:52 +03:00
|
|
|
|
|
|
|
PORT_TABLE_FILE = 'AvailablePorts.md'
|
2021-04-02 12:26:37 +03:00
|
|
|
IGNORE_FILES = {
|
|
|
|
'.gitignore',
|
|
|
|
'.port_include.sh',
|
|
|
|
PORT_TABLE_FILE,
|
|
|
|
'build_all.sh',
|
|
|
|
'build_installed.sh',
|
2021-04-03 06:53:41 +03:00
|
|
|
'README.md',
|
|
|
|
'.hosted_defs.sh'
|
2021-04-02 12:26:37 +03:00
|
|
|
}
|
2021-02-13 15:51:52 +03:00
|
|
|
|
2022-01-08 14:27:38 +03:00
|
|
|
# Matches port names in Ports/foo/ReadMe.md
|
|
|
|
PORT_NAME_REGEX = re.compile(r'([ .()[\]{}\w-]+)\.patch')
|
|
|
|
PORTS_MISSING_DESCRIPTIONS = {
|
|
|
|
'Another-World',
|
|
|
|
'binutils',
|
|
|
|
'chester',
|
|
|
|
'cmatrix',
|
|
|
|
'c-ray',
|
|
|
|
'curl',
|
|
|
|
'dash',
|
|
|
|
'diffutils',
|
|
|
|
'dosbox-staging',
|
|
|
|
'dropbear',
|
|
|
|
'ed',
|
|
|
|
'emu2',
|
|
|
|
'epsilon',
|
|
|
|
'figlet',
|
|
|
|
'flex',
|
|
|
|
'fontconfig',
|
|
|
|
'freeciv',
|
|
|
|
'freedink',
|
|
|
|
'freetype',
|
|
|
|
'gawk',
|
|
|
|
'gcc',
|
|
|
|
'gdb',
|
|
|
|
'genemu',
|
|
|
|
'gettext',
|
|
|
|
'git',
|
|
|
|
'gltron',
|
|
|
|
'gmp',
|
|
|
|
'gnucobol',
|
|
|
|
'gnupg',
|
|
|
|
'gnuplot',
|
|
|
|
'gsl',
|
|
|
|
'harfbuzz',
|
|
|
|
'indent',
|
|
|
|
'jq',
|
|
|
|
'klong',
|
|
|
|
'libassuan',
|
|
|
|
'libgcrypt',
|
|
|
|
'libgd',
|
|
|
|
'libgpg-error',
|
|
|
|
'libiconv',
|
|
|
|
'libicu',
|
|
|
|
'libjpeg',
|
|
|
|
'libksba',
|
|
|
|
'libmodplug',
|
|
|
|
'liboggz',
|
|
|
|
'libpng',
|
|
|
|
'libpuffy',
|
|
|
|
'libsodium',
|
|
|
|
'libvorbis',
|
|
|
|
'libzip',
|
|
|
|
'lua',
|
|
|
|
'm4',
|
|
|
|
'make',
|
|
|
|
'mandoc',
|
|
|
|
'mbedtls',
|
|
|
|
'milkytracker',
|
|
|
|
'mrsh',
|
|
|
|
'mruby',
|
|
|
|
'nano',
|
|
|
|
'ncurses',
|
|
|
|
'neofetch',
|
|
|
|
'nethack',
|
|
|
|
'ninja',
|
|
|
|
'npiet',
|
|
|
|
'npth',
|
|
|
|
'ntbtls',
|
|
|
|
'nyancat',
|
|
|
|
'oksh',
|
|
|
|
'openssh',
|
|
|
|
'openssl',
|
|
|
|
'openttd',
|
|
|
|
'opentyrian',
|
|
|
|
'p7zip',
|
|
|
|
'patch',
|
|
|
|
'pcre2',
|
|
|
|
'pfetch',
|
|
|
|
'php',
|
|
|
|
'pkgconf',
|
|
|
|
'pt2-clone',
|
|
|
|
'qt6-qtbase',
|
|
|
|
'ruby',
|
|
|
|
'sam',
|
|
|
|
'scummvm',
|
|
|
|
'SDL2_image',
|
|
|
|
'SDL2_mixer',
|
|
|
|
'SDL2_net',
|
|
|
|
'SDL2_ttf',
|
|
|
|
'sl',
|
|
|
|
'sqlite',
|
|
|
|
'tcl',
|
|
|
|
'tinycc',
|
|
|
|
'tr',
|
|
|
|
'tuxracer',
|
|
|
|
'vitetris',
|
|
|
|
'wget',
|
|
|
|
'xz',
|
|
|
|
'zsh',
|
|
|
|
'zstd',
|
|
|
|
}
|
|
|
|
|
|
|
|
# FIXME: Once everything is converted into `git format-patch`-style patches,
|
|
|
|
# enable this to allow only `git format-patch` patches.
|
|
|
|
REQUIRE_GIT_PATCHES = False
|
|
|
|
GIT_PATCH_SUBJECT_RE = re.compile(r'Subject: (.*)\n')
|
|
|
|
|
2021-02-13 15:51:52 +03:00
|
|
|
|
|
|
|
def read_port_table(filename):
|
2021-04-02 12:26:37 +03:00
|
|
|
"""Open a file and find all PORT_TABLE_REGEX matches.
|
|
|
|
|
|
|
|
Args:
|
2021-04-29 22:46:15 +03:00
|
|
|
filename (str): filename
|
2021-04-02 12:26:37 +03:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
set: all PORT_TABLE_REGEX matches
|
|
|
|
"""
|
2021-04-23 14:53:53 +03:00
|
|
|
ports = {}
|
2021-02-13 15:51:52 +03:00
|
|
|
with open(filename, 'r') as fp:
|
2021-04-23 14:53:53 +03:00
|
|
|
matches = PORT_TABLE_REGEX.findall(fp.read())
|
|
|
|
for match in matches:
|
|
|
|
line_len = sum([len(part) for part in match])
|
|
|
|
ports[match[0]] = {
|
|
|
|
"dir_ref": match[1],
|
|
|
|
"name": match[2].strip(),
|
|
|
|
"version": match[4].strip(),
|
|
|
|
"url": match[5].strip(),
|
|
|
|
"line_len": line_len
|
|
|
|
}
|
|
|
|
return ports
|
2021-02-13 15:51:52 +03:00
|
|
|
|
|
|
|
|
|
|
|
def read_port_dirs():
|
2021-04-02 12:26:37 +03:00
|
|
|
"""Check Ports directory for unexpected files and check each port has a package.sh file.
|
|
|
|
|
|
|
|
Returns:
|
2021-04-16 20:40:23 +03:00
|
|
|
list: all ports (set), no errors encountered (bool)
|
2021-04-02 12:26:37 +03:00
|
|
|
"""
|
|
|
|
|
2021-04-23 14:53:53 +03:00
|
|
|
ports = {}
|
2021-02-13 15:51:52 +03:00
|
|
|
all_good = True
|
|
|
|
for entry in os.listdir():
|
|
|
|
if entry in IGNORE_FILES:
|
|
|
|
continue
|
|
|
|
if not os.path.isdir(entry):
|
2021-04-16 20:40:23 +03:00
|
|
|
print(f"Ports/{entry} is neither a port (not a directory) nor an ignored file?!")
|
2021-02-13 15:51:52 +03:00
|
|
|
all_good = False
|
|
|
|
continue
|
|
|
|
if not os.path.exists(entry + '/package.sh'):
|
2021-04-16 20:40:23 +03:00
|
|
|
print(f"Ports/{entry}/ is missing its package.sh?!")
|
2021-02-13 15:51:52 +03:00
|
|
|
all_good = False
|
|
|
|
continue
|
2021-04-23 14:53:53 +03:00
|
|
|
ports[entry] = get_port_properties(entry)
|
2021-02-13 15:51:52 +03:00
|
|
|
|
|
|
|
return ports, all_good
|
|
|
|
|
|
|
|
|
2021-04-23 14:53:53 +03:00
|
|
|
PORT_PROPERTIES = ('port', 'version', 'files', 'auth_type')
|
|
|
|
|
|
|
|
|
|
|
|
def get_port_properties(port):
|
|
|
|
"""Retrieves common port properties from its package.sh file.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
dict: keys are values from PORT_PROPERTIES, values are from the package.sh file
|
|
|
|
"""
|
|
|
|
|
|
|
|
props = {}
|
2022-01-17 12:49:23 +03:00
|
|
|
package_sh_command = f"./package.sh showproperty {' '.join(PORT_PROPERTIES)}"
|
|
|
|
res = subprocess.run(f"cd {port}; exec {package_sh_command}", shell=True, capture_output=True)
|
|
|
|
if res.returncode == 0:
|
|
|
|
results = res.stdout.decode('utf-8').split('\n\n')
|
|
|
|
props = {prop: results[i].strip() for i, prop in enumerate(PORT_PROPERTIES)}
|
|
|
|
else:
|
|
|
|
print((
|
|
|
|
f'Executing "{package_sh_command}" script for port {port} failed with '
|
|
|
|
f'exit code {res.returncode}, output from stderr:\n{res.stderr.decode("utf-8").strip()}'
|
|
|
|
))
|
|
|
|
props = {x: '' for x in PORT_PROPERTIES}
|
2021-04-23 14:53:53 +03:00
|
|
|
return props
|
|
|
|
|
|
|
|
|
2021-04-16 20:40:23 +03:00
|
|
|
def check_package_files(ports):
|
|
|
|
"""Check port package.sh file for required properties.
|
|
|
|
|
|
|
|
Args:
|
2021-04-23 14:53:53 +03:00
|
|
|
ports (list): List of all ports to check
|
2021-04-16 20:40:23 +03:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: no errors encountered
|
|
|
|
"""
|
|
|
|
|
|
|
|
all_good = True
|
2022-01-17 11:52:42 +03:00
|
|
|
for port in ports.keys():
|
2021-04-16 20:40:23 +03:00
|
|
|
package_file = f"{port}/package.sh"
|
|
|
|
if not os.path.exists(package_file):
|
|
|
|
continue
|
2022-01-17 11:52:42 +03:00
|
|
|
props = ports[port]
|
2021-04-25 12:54:14 +03:00
|
|
|
if not props['auth_type'] in ('sha256', 'sig', ''):
|
|
|
|
print(f"Ports/{port} uses invalid signature algorithm '{props['auth_type']}' for 'auth_type'")
|
|
|
|
all_good = False
|
|
|
|
|
2021-04-23 14:53:53 +03:00
|
|
|
for prop in PORT_PROPERTIES:
|
2021-08-30 21:48:55 +03:00
|
|
|
if prop == 'auth_type' and re.match('^https://github.com/SerenityPorts/', props["files"]):
|
2021-04-23 14:53:53 +03:00
|
|
|
continue
|
|
|
|
if props[prop] == '':
|
|
|
|
print(f"Ports/{port} is missing required property '{prop}'")
|
|
|
|
all_good = False
|
|
|
|
|
|
|
|
return all_good
|
|
|
|
|
|
|
|
|
2022-01-08 14:27:38 +03:00
|
|
|
def get_and_check_port_patch_list(ports):
|
|
|
|
"""Checks all port patches and returns the port list/properties
|
|
|
|
|
|
|
|
Args:
|
|
|
|
ports (list): List of all ports to check
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
all_good (bool): No errors encountered
|
|
|
|
all_properties (dict): Mapping of port to port properties
|
|
|
|
"""
|
|
|
|
all_port_properties = {}
|
|
|
|
all_good = True
|
|
|
|
|
|
|
|
for port in ports:
|
|
|
|
patches_directory = f"{port}/patches"
|
|
|
|
|
|
|
|
if not os.path.exists(patches_directory):
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not os.path.isdir(patches_directory):
|
|
|
|
print(f"Ports/{port}/patches exists, but is not a directory. This is not right!")
|
|
|
|
all_good = False
|
|
|
|
continue
|
|
|
|
|
|
|
|
patches_path = Path(patches_directory)
|
|
|
|
patches_readme_path = patches_path / "ReadMe.md"
|
|
|
|
patch_files = set(patches_path.glob("*.patch"))
|
|
|
|
non_patch_files = set(patches_path.glob("*")) - patch_files - {patches_readme_path}
|
|
|
|
|
|
|
|
port_properties = {
|
|
|
|
"patches_path": patches_path,
|
|
|
|
"patches_readme_path": patches_readme_path,
|
|
|
|
"patch_files": patch_files,
|
|
|
|
"non_patch_files": non_patch_files
|
|
|
|
}
|
|
|
|
all_port_properties[port] = port_properties
|
|
|
|
|
|
|
|
if len(non_patch_files) != 0:
|
|
|
|
print("Ports/{port}/patches contains the following non-patch files:",
|
|
|
|
', '.join(x.name for x in non_patch_files))
|
|
|
|
all_good = False
|
|
|
|
|
|
|
|
return all_good, all_port_properties
|
|
|
|
|
|
|
|
|
|
|
|
def check_descriptions_for_port_patches(patches):
|
|
|
|
"""Ensure that ports containing patches have them documented.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
patches (dict): Dictionary mapping ports to all their patches
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: no errors encountered
|
|
|
|
"""
|
|
|
|
|
|
|
|
all_good = True
|
|
|
|
for port, properties in patches.items():
|
|
|
|
patches_readme_path = properties["patches_readme_path"]
|
|
|
|
patch_files = properties["patch_files"]
|
|
|
|
|
|
|
|
readme_file_exists = patches_readme_path.exists()
|
|
|
|
if len(patch_files) == 0:
|
|
|
|
print(f"Ports/{port}/patches exists, but contains no patches", end="")
|
|
|
|
if readme_file_exists:
|
|
|
|
print(", yet it contains a ReadMe.md")
|
|
|
|
else:
|
|
|
|
print()
|
|
|
|
all_good = False
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not readme_file_exists:
|
|
|
|
if port not in PORTS_MISSING_DESCRIPTIONS:
|
|
|
|
print(f"Ports/{port}/patches contains patches but no ReadMe.md describing them")
|
|
|
|
all_good = False
|
|
|
|
continue
|
|
|
|
|
|
|
|
with open(str(patches_readme_path), 'r', encoding='utf-8') as f:
|
|
|
|
readme_contents = []
|
|
|
|
for line in f:
|
|
|
|
if not line.startswith('#'):
|
|
|
|
continue
|
|
|
|
match = PORT_NAME_REGEX.search(line)
|
|
|
|
if match:
|
|
|
|
readme_contents.append(match.group(1))
|
|
|
|
|
|
|
|
patch_names = set(Path(x).stem for x in patch_files)
|
|
|
|
|
|
|
|
patches_ok = True
|
|
|
|
for patch_name in patch_names:
|
|
|
|
if patch_name not in readme_contents:
|
|
|
|
if port not in PORTS_MISSING_DESCRIPTIONS:
|
|
|
|
print(f"Ports/{port}/patches/{patch_name}.patch does not appear to be described in"
|
|
|
|
" the corresponding ReadMe.md")
|
|
|
|
all_good = False
|
|
|
|
patches_ok = False
|
|
|
|
|
|
|
|
for patch_name in readme_contents:
|
|
|
|
if patch_name not in patch_names:
|
|
|
|
if port not in PORTS_MISSING_DESCRIPTIONS:
|
|
|
|
print(f"Ports/{port}/patches/{patch_name}.patch is described in ReadMe.md, "
|
|
|
|
"but does not actually exist")
|
|
|
|
all_good = False
|
|
|
|
patches_ok = False
|
|
|
|
|
|
|
|
if port in PORTS_MISSING_DESCRIPTIONS and patches_ok:
|
|
|
|
print(f"Ports/{port}/patches are all described correctly, but the port is marked "
|
|
|
|
"as MISSING_DESCRIPTIONS, make sure to remove it from the list in lint-ports.py")
|
|
|
|
all_good = False
|
|
|
|
|
|
|
|
return all_good
|
|
|
|
|
|
|
|
|
|
|
|
def try_parse_git_patch(path_to_patch):
|
|
|
|
with open(path_to_patch, 'rb') as f:
|
|
|
|
contents_of_patch = f.read()
|
|
|
|
|
|
|
|
with NamedTemporaryFile('r+b') as message_file:
|
|
|
|
res = subprocess.run(
|
|
|
|
f"git mailinfo {message_file.name} /dev/null",
|
|
|
|
shell=True,
|
|
|
|
capture_output=True,
|
|
|
|
input=contents_of_patch)
|
|
|
|
|
|
|
|
if res.returncode != 0:
|
|
|
|
return None
|
|
|
|
|
|
|
|
message = message_file.read().decode('utf-8')
|
|
|
|
subject = GIT_PATCH_SUBJECT_RE.search(res.stdout.decode("utf-8"))
|
|
|
|
if subject:
|
|
|
|
message = subject.group(1) + "\n" + message
|
|
|
|
|
|
|
|
return message
|
|
|
|
|
|
|
|
|
|
|
|
def check_patches_are_git_patches(patches):
|
|
|
|
"""Ensure that all patches are patches made by (or compatible with) `git format-patch`.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
patches (dict): Dictionary mapping ports to all their patches
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: no errors encountered
|
|
|
|
"""
|
|
|
|
|
|
|
|
all_good = True
|
|
|
|
|
|
|
|
for port, properties in patches.items():
|
|
|
|
for patch_path in properties["patch_files"]:
|
|
|
|
result = try_parse_git_patch(patch_path)
|
|
|
|
if not result:
|
|
|
|
print(f"Ports/{port}/patches: {patch_path.stem} does not appear to be a valid "
|
|
|
|
"git patch.")
|
|
|
|
all_good = False
|
|
|
|
continue
|
|
|
|
return all_good
|
|
|
|
|
|
|
|
|
2021-04-23 14:53:53 +03:00
|
|
|
def check_available_ports(from_table, ports):
|
|
|
|
"""Check AvailablePorts.md for correct properties.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
from_table (dict): Ports table from AvailablePorts.md
|
|
|
|
ports (dict): Dictionary with port properties from package.sh
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: no errors encountered
|
|
|
|
"""
|
|
|
|
|
|
|
|
all_good = True
|
|
|
|
|
|
|
|
previous_line_len = None
|
|
|
|
|
|
|
|
for port in from_table.keys():
|
|
|
|
if previous_line_len is None:
|
|
|
|
previous_line_len = from_table[port]["line_len"]
|
|
|
|
if previous_line_len != from_table[port]["line_len"]:
|
|
|
|
print(f"Table row for port {port} is improperly aligned with other rows.")
|
|
|
|
all_good = False
|
|
|
|
else:
|
|
|
|
previous_line_len = from_table[port]["line_len"]
|
|
|
|
|
|
|
|
actual_ref = from_table[port]["dir_ref"]
|
|
|
|
expected_ref = f"{port}/"
|
|
|
|
if actual_ref != expected_ref:
|
|
|
|
print((
|
|
|
|
f'Directory link target in AvailablePorts.md for port {port} is '
|
|
|
|
f'incorrect, expected "{expected_ref}", found "{actual_ref}"'
|
|
|
|
))
|
|
|
|
all_good = False
|
|
|
|
|
|
|
|
actual_version = from_table[port]["version"]
|
|
|
|
expected_version = ports[port]["version"]
|
|
|
|
if GIT_HASH_REGEX.match(expected_version):
|
|
|
|
expected_version = expected_version[0:7]
|
|
|
|
if expected_version == "git":
|
|
|
|
expected_version = ""
|
|
|
|
if actual_version != expected_version:
|
|
|
|
print((
|
|
|
|
f'Version in AvailablePorts.md for port {port} is incorrect, '
|
|
|
|
f'expected "{expected_version}", found "{actual_version}"'
|
|
|
|
))
|
|
|
|
all_good = False
|
2021-04-16 20:40:23 +03:00
|
|
|
|
|
|
|
return all_good
|
|
|
|
|
|
|
|
|
2021-02-13 15:51:52 +03:00
|
|
|
def run():
|
2021-04-16 20:40:23 +03:00
|
|
|
"""Check Ports directory and package files for errors."""
|
2021-04-02 12:26:37 +03:00
|
|
|
|
2021-02-13 15:51:52 +03:00
|
|
|
from_table = read_port_table(PORT_TABLE_FILE)
|
2021-04-16 20:40:23 +03:00
|
|
|
ports, all_good = read_port_dirs()
|
2021-02-13 15:51:52 +03:00
|
|
|
|
2021-04-23 14:53:53 +03:00
|
|
|
from_table_set = set(from_table.keys())
|
|
|
|
ports_set = set(ports.keys())
|
|
|
|
|
|
|
|
if from_table_set - ports_set:
|
2021-02-13 15:51:52 +03:00
|
|
|
all_good = False
|
|
|
|
print('AvailablePorts.md lists ports that do not appear in the file system:')
|
2021-05-07 22:08:45 +03:00
|
|
|
for port in sorted(from_table_set - ports_set):
|
2021-04-16 20:40:23 +03:00
|
|
|
print(f" {port}")
|
2021-02-13 15:51:52 +03:00
|
|
|
|
2021-04-23 14:53:53 +03:00
|
|
|
if ports_set - from_table_set:
|
2021-02-13 15:51:52 +03:00
|
|
|
all_good = False
|
|
|
|
print('AvailablePorts.md is missing the following ports:')
|
2021-04-23 14:53:53 +03:00
|
|
|
for port in sorted(ports_set - from_table_set):
|
2021-04-16 20:40:23 +03:00
|
|
|
print(f" {port}")
|
|
|
|
|
2022-01-17 11:52:42 +03:00
|
|
|
if not check_package_files(ports):
|
2021-04-23 14:53:53 +03:00
|
|
|
all_good = False
|
|
|
|
|
|
|
|
if not check_available_ports(from_table, ports):
|
2021-04-16 20:40:23 +03:00
|
|
|
all_good = False
|
2021-02-13 15:51:52 +03:00
|
|
|
|
2022-01-08 14:27:38 +03:00
|
|
|
patch_list_good, port_properties = get_and_check_port_patch_list(ports.keys())
|
|
|
|
all_good = all_good and patch_list_good
|
|
|
|
|
|
|
|
if not check_descriptions_for_port_patches(port_properties):
|
|
|
|
all_good = False
|
|
|
|
|
|
|
|
if REQUIRE_GIT_PATCHES and not check_patches_are_git_patches(port_properties):
|
|
|
|
all_good = False
|
|
|
|
|
2021-02-13 15:51:52 +03:00
|
|
|
if not all_good:
|
2021-04-02 12:26:37 +03:00
|
|
|
sys.exit(1)
|
2021-02-13 15:51:52 +03:00
|
|
|
|
|
|
|
print('No issues found.')
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2021-04-16 20:40:23 +03:00
|
|
|
os.chdir(f"{os.path.dirname(__file__)}/../Ports")
|
2021-02-13 15:51:52 +03:00
|
|
|
run()
|