scripts: Make gen_app_partitions.py cmake linker generator savvy
Tweak gen_app_partitions.py to be able to generate cmake linker generator output as well as ld-script fragments. Signed-off-by: Björn Bergman <bjorn.bergman@iar.com>
This commit is contained in:
parent
6f74d6fa90
commit
6c197f8961
1 changed files with 285 additions and 63 deletions
|
@ -27,11 +27,148 @@ This script takes as inputs:
|
||||||
- key/value pairs mapping static library files to what partitions their globals
|
- key/value pairs mapping static library files to what partitions their globals
|
||||||
should end up in.
|
should end up in.
|
||||||
|
|
||||||
The output is a linker script fragment containing the definition of the
|
The output is either a linker script fragment or linker-script generator
|
||||||
app shared memory section, which is further divided, for each partition
|
fragments containing the definition of the app shared memory section, which
|
||||||
found, into data and BSS for each partition.
|
is further divided, for each partition found, into data and BSS for each
|
||||||
|
partition.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
#The linker script fragments looks something like this:
|
||||||
|
# SECTION_PROLOGUE(_APP_SMEM_SECTION_NAME,,)
|
||||||
|
# {
|
||||||
|
# APP_SHARED_ALIGN;
|
||||||
|
# _app_smem_start = .;
|
||||||
|
|
||||||
|
# /* Auto generated code do not modify */
|
||||||
|
# SMEM_PARTITION_ALIGN(z_data_smem_ztest_mem_partition_bss_end - z_data_smem_ztest_mem_partition_part_start);
|
||||||
|
# z_data_smem_ztest_mem_partition_part_start = .;
|
||||||
|
# KEEP(*(data_smem_ztest_mem_partition_data*))
|
||||||
|
|
||||||
|
# z_data_smem_ztest_mem_partition_bss_start = .;
|
||||||
|
# KEEP(*(data_smem_ztest_mem_partition_bss*))
|
||||||
|
|
||||||
|
# z_data_smem_ztest_mem_partition_bss_end = .;
|
||||||
|
# SMEM_PARTITION_ALIGN(z_data_smem_ztest_mem_partition_bss_end - z_data_smem_ztest_mem_partition_part_start);
|
||||||
|
# z_data_smem_ztest_mem_partition_part_end = .;
|
||||||
|
|
||||||
|
# /* Auto generated code do not modify */
|
||||||
|
# SMEM_PARTITION_ALIGN(z_data_smem_z_libc_partition_bss_end - z_data_smem_z_libc_partition_part_start);
|
||||||
|
# z_data_smem_z_libc_partition_part_start = .;
|
||||||
|
# KEEP(*(data_smem_z_libc_partition_data*))
|
||||||
|
|
||||||
|
# z_data_smem_z_libc_partition_bss_start = .;
|
||||||
|
# KEEP(*(data_smem_z_libc_partition_bss*))
|
||||||
|
|
||||||
|
# z_data_smem_z_libc_partition_bss_end = .;
|
||||||
|
# SMEM_PARTITION_ALIGN(z_data_smem_z_libc_partition_bss_end - z_data_smem_z_libc_partition_part_start);
|
||||||
|
# z_data_smem_z_libc_partition_part_end = .;
|
||||||
|
|
||||||
|
# APP_SHARED_ALIGN;
|
||||||
|
# _app_smem_end = .;
|
||||||
|
# } GROUP_DATA_LINK_IN(RAMABLE_REGION, ROMABLE_REGION)
|
||||||
|
|
||||||
|
# z_data_smem_ztest_mem_partition_part_size = z_data_smem_ztest_mem_partition_part_end - z_data_smem_ztest_mem_partition_part_start;
|
||||||
|
# z_data_smem_ztest_mem_partition_bss_size = z_data_smem_ztest_mem_partition_bss_end - z_data_smem_ztest_mem_partition_bss_start;
|
||||||
|
|
||||||
|
# z_data_smem_z_libc_partition_part_size = z_data_smem_z_libc_partition_part_end - z_data_smem_z_libc_partition_part_start;
|
||||||
|
# z_data_smem_z_libc_partition_bss_size = z_data_smem_z_libc_partition_bss_end - z_data_smem_z_libc_partition_bss_start;
|
||||||
|
|
||||||
|
# the linker script generator fragments looks something like this:
|
||||||
|
# There is a zephyr_linker_group() called APP_SMEM_GROUP for these sections
|
||||||
|
# defined in linker.cmake
|
||||||
|
# _APP_SMEM_SECTION_NAME is #defined in sections.h to "app_smem". Ideally we should have that here too.
|
||||||
|
#
|
||||||
|
# zephyr_linker_section(NAME _APP_SMEM_SECTION_NAME GROUP APP_SMEM_GROUP NOINPUT ALIGN_WITH_INPUT)
|
||||||
|
# zephyr_linker_section_configure(
|
||||||
|
# SECTION
|
||||||
|
# _APP_SMEM_SECTION_NAME
|
||||||
|
# SYMBOLS
|
||||||
|
# _app_smem_start
|
||||||
|
# )
|
||||||
|
# zephyr_linker_section_configure(
|
||||||
|
# SECTION
|
||||||
|
# _APP_SMEM_SECTION_NAME
|
||||||
|
# INPUT
|
||||||
|
# "data_smem_ztest_mem_partition_data*"
|
||||||
|
# KEEP
|
||||||
|
# SYMBOLS
|
||||||
|
# z_data_smem_ztest_mem_partition_part_start
|
||||||
|
# )
|
||||||
|
# zephyr_linker_section_configure(
|
||||||
|
# SECTION
|
||||||
|
# _APP_SMEM_SECTION_NAME
|
||||||
|
# INPUT
|
||||||
|
# "data_smem_ztest_mem_partition_bss*"
|
||||||
|
# KEEP
|
||||||
|
# SYMBOLS
|
||||||
|
# z_data_smem_ztest_mem_partition_bss_start
|
||||||
|
# z_data_smem_ztest_mem_partition_bss_end
|
||||||
|
# )
|
||||||
|
# zephyr_linker_section_configure(
|
||||||
|
# SECTION
|
||||||
|
# _APP_SMEM_SECTION_NAME
|
||||||
|
# SYMBOLS
|
||||||
|
# z_data_smem_ztest_mem_partition_part_end
|
||||||
|
# )
|
||||||
|
|
||||||
|
# zephyr_linker_section_configure(
|
||||||
|
# SECTION
|
||||||
|
# _APP_SMEM_SECTION_NAME
|
||||||
|
# INPUT
|
||||||
|
# "data_smem_z_libc_partition_data*"
|
||||||
|
# KEEP
|
||||||
|
# SYMBOLS
|
||||||
|
# z_data_smem_z_libc_partition_part_start
|
||||||
|
# )
|
||||||
|
# zephyr_linker_section_configure(
|
||||||
|
# SECTION
|
||||||
|
# _APP_SMEM_SECTION_NAME
|
||||||
|
# INPUT
|
||||||
|
# "data_smem_z_libc_partition_bss*"
|
||||||
|
# KEEP
|
||||||
|
# SYMBOLS
|
||||||
|
# z_data_smem_z_libc_partition_bss_start
|
||||||
|
# z_data_smem_z_libc_partition_bss_end
|
||||||
|
# )
|
||||||
|
# zephyr_linker_section_configure(
|
||||||
|
# SECTION
|
||||||
|
# _APP_SMEM_SECTION_NAME
|
||||||
|
# SYMBOLS
|
||||||
|
# z_data_smem_z_libc_partition_part_end
|
||||||
|
# )
|
||||||
|
# zephyr_linker_section_configure(
|
||||||
|
# SECTION
|
||||||
|
# _APP_SMEM_SECTION_NAME
|
||||||
|
# SYMBOLS
|
||||||
|
# _app_smem_end
|
||||||
|
# )
|
||||||
|
# zephyr_linker_symbol(
|
||||||
|
# SYMBOL
|
||||||
|
# z_data_smem_ztest_mem_partition_part_size
|
||||||
|
# EXPR
|
||||||
|
# "(@z_data_smem_ztest_mem_partition_part_end@ - @z_data_smem_ztest_mem_partition_part_start@)"
|
||||||
|
# )
|
||||||
|
# zephyr_linker_symbol(
|
||||||
|
# SYMBOL
|
||||||
|
# z_data_smem_ztest_mem_partition_bss_size
|
||||||
|
# EXPR
|
||||||
|
# "(@z_data_smem_ztest_mem_partition_bss_end@ - @z_data_smem_ztest_mem_partition_bss_start@)"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# zephyr_linker_symbol(
|
||||||
|
# SYMBOL
|
||||||
|
# z_data_smem_z_libc_partition_part_size
|
||||||
|
# EXPR
|
||||||
|
# "(@z_data_smem_z_libc_partition_part_end@ - @z_data_smem_z_libc_partition_part_start@)"
|
||||||
|
# )
|
||||||
|
# zephyr_linker_symbol(
|
||||||
|
# SYMBOL
|
||||||
|
# z_data_smem_z_libc_partition_bss_size
|
||||||
|
# EXPR
|
||||||
|
# "(@z_data_smem_z_libc_partition_bss_end@ - @z_data_smem_z_libc_partition_bss_start@)"
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
@ -50,60 +187,133 @@ LIB = 'libraries'
|
||||||
# application shared memory partitions.
|
# application shared memory partitions.
|
||||||
# these are later read by the macros defined in app_memdomain.h for
|
# these are later read by the macros defined in app_memdomain.h for
|
||||||
# initialization purpose when USERSPACE is enabled.
|
# initialization purpose when USERSPACE is enabled.
|
||||||
data_template = """
|
|
||||||
/* Auto generated code do not modify */
|
|
||||||
SMEM_PARTITION_ALIGN(z_data_smem_{0}_bss_end - z_data_smem_{0}_part_start);
|
|
||||||
z_data_smem_{0}_part_start = .;
|
|
||||||
KEEP(*(data_smem_{0}_data*))
|
|
||||||
"""
|
|
||||||
|
|
||||||
library_data_template = """
|
class LDScriptTemlate:
|
||||||
\"*{0}:*\"(.data .data.* .sdata .sdata.*)
|
data_template = """
|
||||||
"""
|
/* Auto generated code do not modify */
|
||||||
|
SMEM_PARTITION_ALIGN(z_data_smem_{partition}_bss_end - z_data_smem_{partition}_part_start);
|
||||||
|
z_data_smem_{partition}_part_start = .;
|
||||||
|
KEEP(*(data_smem_{partition}_data*))
|
||||||
|
"""
|
||||||
|
|
||||||
bss_template = """
|
library_data_template = """
|
||||||
z_data_smem_{0}_bss_start = .;
|
\"*{lib}:*\"(.data .data.* .sdata .sdata.*)
|
||||||
KEEP(*(data_smem_{0}_bss*))
|
"""
|
||||||
"""
|
|
||||||
|
|
||||||
library_bss_template = """
|
bss_template = """
|
||||||
\"*{0}:*\"(.bss .bss.* .sbss .sbss.* COMMON COMMON.*)
|
z_data_smem_{partition}_bss_start = .;
|
||||||
"""
|
KEEP(*(data_smem_{partition}_bss*))
|
||||||
|
"""
|
||||||
|
|
||||||
footer_template = """
|
library_bss_template = """
|
||||||
z_data_smem_{0}_bss_end = .;
|
\"*{lib}:*\"(.bss .bss.* .sbss .sbss.* COMMON COMMON.*)
|
||||||
SMEM_PARTITION_ALIGN(z_data_smem_{0}_bss_end - z_data_smem_{0}_part_start);
|
"""
|
||||||
z_data_smem_{0}_part_end = .;
|
|
||||||
"""
|
|
||||||
|
|
||||||
linker_start_seq = """
|
footer_template = """
|
||||||
SECTION_PROLOGUE(_APP_SMEM{1}_SECTION_NAME,,)
|
z_data_smem_{partition}_bss_end = .;
|
||||||
{{
|
SMEM_PARTITION_ALIGN(z_data_smem_{partition}_bss_end - z_data_smem_{partition}_part_start);
|
||||||
APP_SHARED_ALIGN;
|
z_data_smem_{partition}_part_end = .;
|
||||||
_app_smem{0}_start = .;
|
"""
|
||||||
"""
|
|
||||||
|
|
||||||
linker_end_seq = """
|
linker_start_seq = """
|
||||||
APP_SHARED_ALIGN;
|
SECTION_PROLOGUE(_APP_SMEM{SECTION}_SECTION_NAME,,)
|
||||||
_app_smem{0}_end = .;
|
{{
|
||||||
}} GROUP_DATA_LINK_IN(RAMABLE_REGION, ROMABLE_REGION)
|
APP_SHARED_ALIGN;
|
||||||
"""
|
_app_smem{section}_start = .;
|
||||||
|
"""
|
||||||
|
|
||||||
empty_app_smem = """
|
linker_end_seq = """
|
||||||
SECTION_PROLOGUE(_APP_SMEM{1}_SECTION_NAME,,)
|
APP_SHARED_ALIGN;
|
||||||
{{
|
_app_smem{section}_end = .;
|
||||||
#ifdef EMPTY_APP_SHARED_ALIGN
|
}} GROUP_DATA_LINK_IN(RAMABLE_REGION, ROMABLE_REGION)
|
||||||
EMPTY_APP_SHARED_ALIGN;
|
"""
|
||||||
#endif
|
|
||||||
_app_smem{0}_start = .;
|
|
||||||
_app_smem{0}_end = .;
|
|
||||||
}} GROUP_DATA_LINK_IN(RAMABLE_REGION, ROMABLE_REGION)
|
|
||||||
"""
|
|
||||||
|
|
||||||
size_cal_string = """
|
empty_app_smem = """
|
||||||
z_data_smem_{0}_part_size = z_data_smem_{0}_part_end - z_data_smem_{0}_part_start;
|
SECTION_PROLOGUE(_APP_SMEM{SECTION}_SECTION_NAME,,)
|
||||||
z_data_smem_{0}_bss_size = z_data_smem_{0}_bss_end - z_data_smem_{0}_bss_start;
|
{{
|
||||||
"""
|
#ifdef EMPTY_APP_SHARED_ALIGN
|
||||||
|
EMPTY_APP_SHARED_ALIGN;
|
||||||
|
#endif
|
||||||
|
_app_smem{section}_start = .;
|
||||||
|
_app_smem{section}_end = .;
|
||||||
|
}} GROUP_DATA_LINK_IN(RAMABLE_REGION, ROMABLE_REGION)
|
||||||
|
"""
|
||||||
|
|
||||||
|
size_cal_string = """
|
||||||
|
z_data_smem_{partition}_part_size = z_data_smem_{partition}_part_end - z_data_smem_{partition}_part_start;
|
||||||
|
z_data_smem_{partition}_bss_size = z_data_smem_{partition}_bss_end - z_data_smem_{partition}_bss_start;
|
||||||
|
"""
|
||||||
|
|
||||||
|
def cmake_list_append(list, props):
|
||||||
|
tokens = [fr"{key}\;{value}" for key, value in props.items()]
|
||||||
|
stuff = r"\;".join(tokens)
|
||||||
|
return """list(APPEND """+ list + """ \"{{""" + stuff + """}}\")\n"""
|
||||||
|
|
||||||
|
def zephyr_linker_section(name, group, align = None, noinput = True, align_with_input = True, hidden = False, noinit = False):
|
||||||
|
props = {"""NAME""": name, """GROUP""": group}
|
||||||
|
if align :
|
||||||
|
props['ALIGN'] = align
|
||||||
|
#The bool flags are always there:
|
||||||
|
props['NOIPUT'] = noinput
|
||||||
|
props['ALIGN_WITH_INPUT'] = align_with_input
|
||||||
|
props['HIDDEN'] = hidden
|
||||||
|
props['NOINIT'] = noinit
|
||||||
|
return cmake_list_append("SECTIONS", props)
|
||||||
|
|
||||||
|
def zephyr_linker_section_configure(section, input = None, symbols = None, align = None, any = False, first = False, keep = False):
|
||||||
|
props = {"SECTION": section}
|
||||||
|
if input:
|
||||||
|
props["INPUT"] = input
|
||||||
|
#ANY\;FALSE\;FIRST\;FALSE\;KEEP\;FALSE\
|
||||||
|
props['ANY'] = any
|
||||||
|
props['FIRST'] = first
|
||||||
|
props['KEEP'] = keep
|
||||||
|
if symbols:
|
||||||
|
props["SYMBOLS"] = symbols
|
||||||
|
if align:
|
||||||
|
props["ALIGN"] = align
|
||||||
|
return cmake_list_append("SECTION_SETTINGS", props)
|
||||||
|
|
||||||
|
def zephyr_linker_symbol(symbol, expr) :
|
||||||
|
return cmake_list_append("SYMBOLS", {'SYMBOL': symbol, 'EXPR':expr})
|
||||||
|
|
||||||
|
class CmakeTemplate:
|
||||||
|
section_name = "@_APP_SMEM{SECTION}_SECTION_NAME@"
|
||||||
|
data_template = (
|
||||||
|
zephyr_linker_section_configure(section=section_name, align="@SMEM_PARTITION_ALIGN_BYTES@")+
|
||||||
|
zephyr_linker_section_configure(section=section_name, input="data_smem_{partition}_data*", symbols="z_data_smem_{partition}_part_start", keep=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
library_data_template = zephyr_linker_section_configure(section=section_name, input="*{lib}:*(.data .data.* .sdata .sdata.*)")
|
||||||
|
|
||||||
|
bss_template = (
|
||||||
|
zephyr_linker_section_configure(section=section_name, input="data_smem_{partition}_bss*", symbols="z_data_smem_{partition}_bss_start", keep=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
library_bss_template = zephyr_linker_section_configure(section=section_name, input="*{lib}:*(.bss .bss.* .sbss .sbss.* COMMON COMMON.*)")
|
||||||
|
|
||||||
|
footer_template = (
|
||||||
|
zephyr_linker_section_configure(section=section_name, symbols="z_data_smem_{partition}_bss_end", keep=True) +
|
||||||
|
zephyr_linker_section_configure(section=section_name, align="@SMEM_PARTITION_ALIGN_BYTES@") +
|
||||||
|
zephyr_linker_section_configure(section=section_name, symbols="z_data_smem_{partition}_part_end", keep=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
linker_start_seq = (
|
||||||
|
zephyr_linker_section(name=section_name, group="APP_SMEM_GROUP", noinput=True, align_with_input=True) +
|
||||||
|
zephyr_linker_section_configure(section=section_name, align="@APP_SHARED_ALIGN_BYTES@", symbols="_app_smem{section}_start"))
|
||||||
|
|
||||||
|
linker_end_seq = (
|
||||||
|
zephyr_linker_section_configure(section=section_name, align="@APP_SHARED_ALIGN_BYTES@") +
|
||||||
|
zephyr_linker_section_configure(section=section_name, symbols="_app_smem{section}_end") )
|
||||||
|
|
||||||
|
empty_app_smem = (
|
||||||
|
zephyr_linker_section(name=section_name, group="APP_SMEM_GROUP", align="@APP_SHARED_ALIGN_BYTES@", noinput=True, align_with_input=True) +
|
||||||
|
zephyr_linker_section_configure(section = section_name, symbols="_app_smem{section}_start") +
|
||||||
|
zephyr_linker_section_configure(section = section_name, symbols="_app_smem{section}_end") )
|
||||||
|
|
||||||
|
size_cal_string = (
|
||||||
|
zephyr_linker_symbol(symbol='z_data_smem_{partition}_part_size', expr='@z_data_smem_{partition}_part_end@ - @z_data_smem_{partition}_part_start@') +
|
||||||
|
zephyr_linker_symbol(symbol='z_data_smem_{partition}_bss_size', expr='@z_data_smem_{partition}_bss_end@ - @z_data_smem_{partition}_bss_start@'))
|
||||||
|
|
||||||
section_regex = re.compile(r'data_smem_([A-Za-z0-9_]*)_(data|bss)*')
|
section_regex = re.compile(r'data_smem_([A-Za-z0-9_]*)_(data|bss)*')
|
||||||
|
|
||||||
|
@ -200,34 +410,45 @@ def parse_elf_file(partitions):
|
||||||
else:
|
else:
|
||||||
partitions[partition_name][SZ] += size
|
partitions[partition_name][SZ] += size
|
||||||
|
|
||||||
|
def generate_final(linker_file, partitions, lnkr_sect=""):
|
||||||
|
if linker_file.endswith(".ld"):
|
||||||
|
template = LDScriptTemlate
|
||||||
|
else:
|
||||||
|
template = CmakeTemplate
|
||||||
|
generate_final_linker(template, linker_file, partitions, lnkr_sect)
|
||||||
|
|
||||||
def generate_final_linker(linker_file, partitions, lnkr_sect=""):
|
def generate_final_linker(template, linker_file, partitions, lnkr_sect):
|
||||||
string = ""
|
string = ""
|
||||||
|
props = { 'section' : lnkr_sect, 'SECTION' : lnkr_sect.upper() }
|
||||||
if len(partitions) > 0:
|
if len(partitions) > 0:
|
||||||
string = linker_start_seq.format(lnkr_sect, lnkr_sect.upper())
|
string = template.linker_start_seq.format(**props)
|
||||||
size_string = ''
|
size_string = ''
|
||||||
for partition, item in partitions.items():
|
for partition, item in partitions.items():
|
||||||
string += data_template.format(partition)
|
part_props = props
|
||||||
|
part_props["partition"] = partition
|
||||||
|
string += template.data_template.format(**part_props)
|
||||||
if LIB in item:
|
if LIB in item:
|
||||||
for lib in item[LIB]:
|
for lib in item[LIB]:
|
||||||
string += library_data_template.format(lib)
|
lib_props = part_props
|
||||||
string += bss_template.format(partition, lnkr_sect)
|
lib_props["lib"] = lib
|
||||||
|
string += template.library_data_template.format(**lib_props)
|
||||||
|
string += template.bss_template.format(**part_props)
|
||||||
if LIB in item:
|
if LIB in item:
|
||||||
for lib in item[LIB]:
|
for lib in item[LIB]:
|
||||||
string += library_bss_template.format(lib)
|
lib_props = part_props
|
||||||
string += footer_template.format(partition)
|
lib_props["lib"] = lib
|
||||||
size_string += size_cal_string.format(partition)
|
string += template.library_bss_template.format(**lib_props)
|
||||||
|
string += template.footer_template.format(**part_props)
|
||||||
|
size_string += template.size_cal_string.format(**part_props)
|
||||||
|
|
||||||
string += linker_end_seq.format(lnkr_sect)
|
string += template.linker_end_seq.format(**props)
|
||||||
string += size_string
|
string += size_string
|
||||||
else:
|
else:
|
||||||
string = empty_app_smem.format(lnkr_sect, lnkr_sect.upper())
|
string = template.empty_app_smem.format(**props) #lnkr_sect, lnkr_sect.upper())
|
||||||
|
|
||||||
with open(linker_file, "w") as fw:
|
with open(linker_file, "w") as fw:
|
||||||
fw.write(string)
|
fw.write(string)
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
global args
|
global args
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
@ -294,7 +515,8 @@ def main():
|
||||||
|
|
||||||
partsorted = OrderedDict(decreasing_tuples)
|
partsorted = OrderedDict(decreasing_tuples)
|
||||||
|
|
||||||
generate_final_linker(args.output, partsorted)
|
generate_final(args.output, partsorted)
|
||||||
|
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
print("Partitions retrieved:")
|
print("Partitions retrieved:")
|
||||||
for key in partsorted:
|
for key in partsorted:
|
||||||
|
@ -308,7 +530,7 @@ def main():
|
||||||
|
|
||||||
partsorted = OrderedDict(decreasing_tuples)
|
partsorted = OrderedDict(decreasing_tuples)
|
||||||
|
|
||||||
generate_final_linker(args.pinoutput, partsorted, lnkr_sect="_pinned")
|
generate_final(args.pinoutput, partsorted, lnkr_sect="_pinned")
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
print("Pinned partitions retrieved:")
|
print("Pinned partitions retrieved:")
|
||||||
for key in partsorted:
|
for key in partsorted:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue