Merge branch 'feature/spiffs_image_generator' into 'master'

SPIFFS Image Generator

See merge request idf/esp-idf!4156
This commit is contained in:
Ivan Grokhotkov 2019-04-01 20:08:40 +08:00
commit d52ecb71d6
18 changed files with 4884 additions and 34 deletions

View file

@ -0,0 +1,45 @@
SPIFFSGEN_PY:=$(COMPONENT_PATH)/spiffsgen.py
SPIFFSGEN_FLASH_IN_PROJECT=
ifdef CONFIG_SPIFFS_USE_MAGIC
USE_MAGIC = "--use-magic"
else
USE_MAGIC = ""
endif
ifdef CONFIG_SPIFFS_USE_MAGIC_LENGTH
USE_MAGIC_LEN = "--use-magic-len"
else
USE_MAGIC_LEN = ""
endif
# spiffs_create_partition_image
#
# Create a spiffs image of the specified directory on the host during build and optionally
# have the created image flashed using `make flash`
define spiffs_create_partition_image
$(1)_bin: $(PARTITION_TABLE_BIN) | check_python_dependencies
partition_size=`$(GET_PART_INFO) --partition-name $(1) \
--partition-table-file $(PARTITION_TABLE_BIN) \
get_partition_info --info size`; \
$(PYTHON) $(SPIFFSGEN_PY) $$$$partition_size $(2) $(BUILD_DIR_BASE)/$(1).bin \
--page-size=$(CONFIG_SPIFFS_PAGE_SIZE) \
--obj-name-len=$(CONFIG_SPIFFS_OBJ_NAME_LEN) \
--meta-len=$(CONFIG_SPIFFS_META_LENGTH) \
$(USE_MAGIC) \
$(USE_MAGIC_LEN)
all_binaries: $(1)_bin
print_flash_cmd: $(1)_bin
# Append the created binary to esptool_py args if FLASH_IN_PROJECT is set
ifeq ($(3), FLASH_IN_PROJECT)
SPIFFSGEN_FLASH_IN_PROJECT += $(1)
endif
endef
ESPTOOL_ALL_FLASH_ARGS += $(foreach partition,$(SPIFFSGEN_FLASH_IN_PROJECT), \
$(shell $(GET_PART_INFO) --partition-name $(partition) \
--partition-table-file $(PARTITION_TABLE_BIN) get_partition_info --info offset) $(BUILD_DIR_BASE)/$(partition).bin)

View file

@ -0,0 +1,46 @@
# spiffs_create_partition_image
#
# Create a spiffs image of the specified directory on the host during build and optionally
# have the created image flashed using `idf.py flash`
function(spiffs_create_partition_image partition base_dir)
set(options FLASH_IN_PROJECT)
cmake_parse_arguments(arg "${options}" "" "" "${ARGN}")
set(spiffsgen_py ${PYTHON} ${IDF_PATH}/components/spiffs/spiffsgen.py)
get_filename_component(base_dir_full_path ${base_dir} ABSOLUTE)
partition_table_get_partition_info(size "--partition-name ${partition}" "size")
partition_table_get_partition_info(offset "--partition-name ${partition}" "offset")
set(image_file ${CMAKE_BINARY_DIR}/${partition}.bin)
if(CONFIG_SPIFFS_USE_MAGIC)
set(use_magic "--use-magic")
endif()
if(CONFIG_SPIFFS_USE_MAGIC_LENGTH)
set(use_magic_len "--use-magic-len")
endif()
# Execute SPIFFS image generation; this always executes as there is no way to specify for CMake to watch for
# contents of the base dir changing.
add_custom_target(spiffs_${partition}_bin ALL
COMMAND ${spiffsgen_py} ${size} ${base_dir_full_path} ${image_file}
--page-size=${CONFIG_SPIFFS_PAGE_SIZE}
--obj-name-len=${CONFIG_SPIFFS_OBJ_NAME_LEN}
--meta-len=${CONFIG_SPIFFS_META_LENGTH}
${use_magic}
${use_magic_len}
)
set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY
ADDITIONAL_MAKE_CLEAN_FILES
${image_file})
if(arg_FLASH_IN_PROJECT)
esptool_py_flash_project_args(${partition} ${offset} ${image_file} FLASH_IN_PROJECT)
else()
esptool_py_flash_project_args(${partition} ${offset} ${image_file})
endif()
endfunction()

527
components/spiffs/spiffsgen.py Executable file
View file

@ -0,0 +1,527 @@
#!/usr/bin/env python
#
# spiffsgen is a tool used to generate a spiffs image from a directory
#
# Copyright 2019 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import division
import os
import io
import math
import struct
import argparse
import ctypes
SPIFFS_PH_FLAG_USED_FINAL_INDEX = 0xF8
SPIFFS_PH_FLAG_USED_FINAL = 0xFC
SPIFFS_PH_FLAG_LEN = 1
SPIFFS_PH_IX_SIZE_LEN = 4
SPIFFS_PH_IX_OBJ_TYPE_LEN = 1
SPIFFS_TYPE_FILE = 1
# Based on typedefs under spiffs_config.h
SPIFFS_OBJ_ID_LEN = 2 # spiffs_obj_id
SPIFFS_SPAN_IX_LEN = 2 # spiffs_span_ix
SPIFFS_PAGE_IX_LEN = 2 # spiffs_page_ix
SPIFFS_BLOCK_IX_LEN = 2 # spiffs_block_ix
class SpiffsBuildConfig():
def __init__(self, page_size, page_ix_len, block_size,
block_ix_len, meta_len, obj_name_len, obj_id_len,
span_ix_len, packed, aligned, endianness, use_magic, use_magic_len):
if block_size % page_size != 0:
raise RuntimeError("block size should be a multiple of page size")
self.page_size = page_size
self.block_size = block_size
self.obj_id_len = obj_id_len
self.span_ix_len = span_ix_len
self.packed = packed
self.aligned = aligned
self.obj_name_len = obj_name_len
self.meta_len = meta_len
self.page_ix_len = page_ix_len
self.block_ix_len = block_ix_len
self.endianness = endianness
self.use_magic = use_magic
self.use_magic_len = use_magic_len
self.PAGES_PER_BLOCK = self.block_size // self.page_size
self.OBJ_LU_PAGES_PER_BLOCK = int(math.ceil(self.block_size / self.page_size * self.obj_id_len / self.page_size))
self.OBJ_USABLE_PAGES_PER_BLOCK = self.PAGES_PER_BLOCK - self.OBJ_LU_PAGES_PER_BLOCK
self.OBJ_LU_PAGES_OBJ_IDS_LIM = self.page_size // self.obj_id_len
self.OBJ_DATA_PAGE_HEADER_LEN = self.obj_id_len + self.span_ix_len + SPIFFS_PH_FLAG_LEN
pad = 4 - (4 if self.OBJ_DATA_PAGE_HEADER_LEN % 4 == 0 else self.OBJ_DATA_PAGE_HEADER_LEN % 4)
self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED = self.OBJ_DATA_PAGE_HEADER_LEN + pad
self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD = pad
self.OBJ_DATA_PAGE_CONTENT_LEN = self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN
self.OBJ_INDEX_PAGES_HEADER_LEN = (self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED + SPIFFS_PH_IX_SIZE_LEN +
SPIFFS_PH_IX_OBJ_TYPE_LEN + self.obj_name_len + self.meta_len)
self.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM = (self.page_size - self.OBJ_INDEX_PAGES_HEADER_LEN) // self.block_ix_len
self.OBJ_INDEX_PAGES_OBJ_IDS_LIM = (self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED) / self.block_ix_len
class SpiffsFullError(RuntimeError):
def __init__(self, message=None):
super(SpiffsFullError, self).__init__(message)
class SpiffsPage():
_endianness_dict = {
"little": "<",
"big": ">"
}
_len_dict = {
1: "B",
2: "H",
4: "I",
8: "Q"
}
_type_dict = {
1: ctypes.c_ubyte,
2: ctypes.c_ushort,
4: ctypes.c_uint,
8: ctypes.c_ulonglong
}
def __init__(self, bix, build_config):
self.build_config = build_config
self.bix = bix
class SpiffsObjLuPage(SpiffsPage):
def __init__(self, bix, build_config):
SpiffsPage.__init__(self, bix, build_config)
self.obj_ids_limit = self.build_config.OBJ_LU_PAGES_OBJ_IDS_LIM
self.obj_ids = list()
def _calc_magic(self, blocks_lim):
# Calculate the magic value mirrorring computation done by the macro SPIFFS_MAGIC defined in
# spiffs_nucleus.h
magic = 0x20140529 ^ self.build_config.page_size
if self.build_config.use_magic_len:
magic = magic ^ (blocks_lim - self.bix)
magic = SpiffsPage._type_dict[self.build_config.obj_id_len](magic)
return magic.value
def register_page(self, page):
if not self.obj_ids_limit > 0:
raise SpiffsFullError()
obj_id = (page.obj_id, page.__class__)
self.obj_ids.append(obj_id)
self.obj_ids_limit -= 1
def to_binary(self):
global test
img = b""
for (obj_id, page_type) in self.obj_ids:
if page_type == SpiffsObjIndexPage:
obj_id ^= (1 << ((self.build_config.obj_id_len * 8) - 1))
img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.obj_id_len], obj_id)
assert(len(img) <= self.build_config.page_size)
img += b"\xFF" * (self.build_config.page_size - len(img))
return img
def magicfy(self, blocks_lim):
# Only use magic value if no valid obj id has been written to the spot, which is the
# spot taken up by the last obj id on last lookup page. The parent is responsible
# for determining which is the last lookup page and calling this function.
remaining = self.obj_ids_limit
empty_obj_id_dict = {
1: 0xFF,
2: 0xFFFF,
4: 0xFFFFFFFF,
8: 0xFFFFFFFFFFFFFFFF
}
if (remaining >= 2):
for i in range(remaining):
if i == remaining - 2:
self.obj_ids.append((self._calc_magic(blocks_lim), SpiffsObjDataPage))
break
else:
self.obj_ids.append((empty_obj_id_dict[self.build_config.obj_id_len], SpiffsObjDataPage))
self.obj_ids_limit -= 1
class SpiffsObjIndexPage(SpiffsPage):
def __init__(self, obj_id, span_ix, size, name, build_config):
SpiffsPage.__init__(self, 0, build_config)
self.obj_id = obj_id
self.span_ix = span_ix
self.name = name
self.size = size
if self.span_ix == 0:
self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM
else:
self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_LIM
self.pages = list()
def register_page(self, page):
if not self.pages_lim > 0:
raise SpiffsFullError
self.pages.append(page.offset)
self.pages_lim -= 1
def to_binary(self):
obj_id = self.obj_id ^ (1 << ((self.build_config.obj_id_len * 8) - 1))
img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.obj_id_len] +
SpiffsPage._len_dict[self.build_config.span_ix_len] +
SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN],
obj_id,
self.span_ix,
SPIFFS_PH_FLAG_USED_FINAL_INDEX)
# Add padding before the object index page specific information
img += b"\xFF" * self.build_config.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD
# If this is the first object index page for the object, add filname, type
# and size information
if self.span_ix == 0:
img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[SPIFFS_PH_IX_SIZE_LEN] +
SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN],
self.size,
SPIFFS_TYPE_FILE)
img += self.name.encode() + (b"\x00" * ((self.build_config.obj_name_len - len(self.name)) + self.build_config.meta_len))
# Finally, add the page index of daa pages
for page in self.pages:
page = page >> int(math.log(self.build_config.page_size, 2))
img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.page_ix_len], page)
assert(len(img) <= self.build_config.page_size)
img += b"\xFF" * (self.build_config.page_size - len(img))
return img
class SpiffsObjDataPage(SpiffsPage):
def __init__(self, offset, obj_id, span_ix, contents, build_config):
SpiffsPage.__init__(self, 0, build_config)
self.obj_id = obj_id
self.span_ix = span_ix
self.contents = contents
self.offset = offset
def to_binary(self):
img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.obj_id_len] +
SpiffsPage._len_dict[self.build_config.span_ix_len] +
SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN],
self.obj_id,
self.span_ix,
SPIFFS_PH_FLAG_USED_FINAL)
img += self.contents
assert(len(img) <= self.build_config.page_size)
img += b"\xFF" * (self.build_config.page_size - len(img))
return img
class SpiffsBlock():
def _reset(self):
self.cur_obj_index_span_ix = 0
self.cur_obj_data_span_ix = 0
self.cur_obj_id = 0
self.cur_obj_idx_page = None
def __init__(self, bix, blocks_lim, build_config):
self.build_config = build_config
self.offset = bix * self.build_config.block_size
self.remaining_pages = self.build_config.OBJ_USABLE_PAGES_PER_BLOCK
self.pages = list()
self.bix = bix
lu_pages = list()
for i in range(self.build_config.OBJ_LU_PAGES_PER_BLOCK):
page = SpiffsObjLuPage(self.bix, self.build_config)
lu_pages.append(page)
self.pages.extend(lu_pages)
self.lu_page_iter = iter(lu_pages)
self.lu_page = next(self.lu_page_iter)
self._reset()
def _register_page(self, page):
if isinstance(page, SpiffsObjDataPage):
self.cur_obj_idx_page.register_page(page) # can raise SpiffsFullError
try:
self.lu_page.register_page(page)
except SpiffsFullError:
self.lu_page = next(self.lu_page_iter)
try:
self.lu_page.register_page(page)
except AttributeError: # no next lookup page
# Since the amount of lookup pages is pre-computed at every block instance,
# this should never occur
raise RuntimeError("invalid attempt to add page to a block when there is no more space in lookup")
self.pages.append(page)
def begin_obj(self, obj_id, size, name, obj_index_span_ix=0, obj_data_span_ix=0):
if not self.remaining_pages > 0:
raise SpiffsFullError()
self._reset()
self.cur_obj_id = obj_id
self.cur_obj_index_span_ix = obj_index_span_ix
self.cur_obj_data_span_ix = obj_data_span_ix
page = SpiffsObjIndexPage(obj_id, self.cur_obj_index_span_ix, size, name, self.build_config)
self._register_page(page)
self.cur_obj_idx_page = page
self.remaining_pages -= 1
self.cur_obj_index_span_ix += 1
def update_obj(self, contents):
if not self.remaining_pages > 0:
raise SpiffsFullError()
page = SpiffsObjDataPage(self.offset + (len(self.pages) * self.build_config.page_size),
self.cur_obj_id, self.cur_obj_data_span_ix, contents, self.build_config)
self._register_page(page)
self.cur_obj_data_span_ix += 1
self.remaining_pages -= 1
def end_obj(self):
self._reset()
def is_full(self):
return self.remaining_pages <= 0
def to_binary(self, blocks_lim):
img = b""
if self.build_config.use_magic:
for (idx, page) in enumerate(self.pages):
if idx == self.build_config.OBJ_LU_PAGES_PER_BLOCK - 1:
page.magicfy(blocks_lim)
img += page.to_binary()
else:
for page in self.pages:
img += page.to_binary()
assert(len(img) <= self.build_config.block_size)
img += b"\xFF" * (self.build_config.block_size - len(img))
return img
class SpiffsFS():
def __init__(self, img_size, build_config):
if img_size % build_config.block_size != 0:
raise RuntimeError("image size should be a multiple of block size")
self.img_size = img_size
self.build_config = build_config
self.blocks = list()
self.blocks_lim = self.img_size // self.build_config.block_size
self.remaining_blocks = self.blocks_lim
self.cur_obj_id = 1 # starting object id
def _create_block(self):
if self.is_full():
raise SpiffsFullError("the image size has been exceeded")
block = SpiffsBlock(len(self.blocks), self.blocks_lim, self.build_config)
self.blocks.append(block)
self.remaining_blocks -= 1
return block
def is_full(self):
return self.remaining_blocks <= 0
def create_file(self, img_path, file_path):
contents = None
if len(img_path) > self.build_config.obj_name_len:
raise RuntimeError("object name '%s' too long" % img_path)
name = img_path
with open(file_path, "rb") as obj:
contents = obj.read()
stream = io.BytesIO(contents)
try:
block = self.blocks[-1]
block.begin_obj(self.cur_obj_id, len(contents), name)
except (IndexError, SpiffsFullError):
block = self._create_block()
block.begin_obj(self.cur_obj_id, len(contents), name)
contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN)
while contents_chunk:
try:
block = self.blocks[-1]
try:
# This can fail because either (1) all the pages in block have been
# used or (2) object index has been exhausted.
block.update_obj(contents_chunk)
except SpiffsFullError:
# If its (1), use the outer exception handler
if block.is_full():
raise SpiffsFullError
# If its (2), write another object index page
block.begin_obj(self.cur_obj_id, len(contents), name,
obj_index_span_ix=block.cur_obj_index_span_ix,
obj_data_span_ix=block.cur_obj_data_span_ix)
continue
except (IndexError, SpiffsFullError):
# All pages in the block have been exhausted. Create a new block, copying
# the previous state of the block to a new one for the continuation of the
# current object
prev_block = block
block = self._create_block()
block.cur_obj_id = prev_block.cur_obj_id
block.cur_obj_idx_page = prev_block.cur_obj_idx_page
block.cur_obj_data_span_ix = prev_block.cur_obj_data_span_ix
block.cur_obj_index_span_ix = prev_block.cur_obj_index_span_ix
continue
contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN)
block.end_obj()
self.cur_obj_id += 1
def to_binary(self):
img = b""
for block in self.blocks:
img += block.to_binary(self.blocks_lim)
bix = len(self.blocks)
if self.build_config.use_magic:
# Create empty blocks with magic numbers
while self.remaining_blocks > 0:
block = SpiffsBlock(bix, self.blocks_lim, self.build_config)
img += block.to_binary(self.blocks_lim)
self.remaining_blocks -= 1
bix += 1
else:
# Just fill remaining spaces FF's
img += "\xFF" * (self.img_size - len(img))
return img
def main():
parser = argparse.ArgumentParser(description="SPIFFS Image Generator",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("image_size",
help="Size of the created image")
parser.add_argument("base_dir",
help="Path to directory from which the image will be created")
parser.add_argument("output_file",
help="Created image output file path")
parser.add_argument("--page-size",
help="Logical page size. Set to value same as CONFIG_SPIFFS_PAGE_SIZE.",
type=int,
default=256)
parser.add_argument("--block-size",
help="Logical block size. Set to the same value as the flash chip's sector size (g_rom_flashchip.sector_size).",
type=int,
default=4096)
parser.add_argument("--obj-name-len",
help="File full path maximum length. Set to value same as CONFIG_SPIFFS_OBJ_NAME_LEN.",
type=int,
default=32)
parser.add_argument("--meta-len",
help="File metadata length. Set to value same as CONFIG_SPIFFS_META_LENGTH.",
type=int,
default=4)
parser.add_argument("--use-magic",
help="Use magic number to create an identifiable SPIFFS image. Specify if CONFIG_SPIFFS_USE_MAGIC.",
action="store_true",
default=True)
parser.add_argument("--use-magic-len",
help="Use position in memory to create different magic numbers for each block. Specify if CONFIG_SPIFFS_USE_MAGIC_LENGTH.",
action="store_true",
default=True)
parser.add_argument("--big-endian",
help="Specify if the target architecture is big-endian. If not specified, little-endian is assumed.",
action="store_true",
default=False)
args = parser.parse_args()
if not os.path.exists(args.base_dir):
raise RuntimeError("given base directory %s does not exist" % args.base_dir)
with open(args.output_file, "wb") as image_file:
image_size = int(args.image_size, 0)
spiffs_build_default = SpiffsBuildConfig(args.page_size, SPIFFS_PAGE_IX_LEN,
args.block_size, SPIFFS_BLOCK_IX_LEN, args.meta_len,
args.obj_name_len, SPIFFS_OBJ_ID_LEN, SPIFFS_SPAN_IX_LEN,
True, True, "big" if args.big_endian else "little",
args.use_magic, args.use_magic_len)
spiffs = SpiffsFS(image_size, spiffs_build_default)
for root, dirs, files in os.walk(args.base_dir):
for f in files:
full_path = os.path.join(root, f)
spiffs.create_file("/" + os.path.relpath(full_path, args.base_dir), full_path)
image = spiffs.to_binary()
image_file.write(image)
if __name__ == "__main__":
main()

View file

@ -68,7 +68,7 @@ $(BUILD_DIR)/$(COMPONENT_LIB): $(OBJ_FILES) $(SDKCONFIG)
clean:
$(MAKE) -C $(STUBS_LIB_DIR) clean
$(MAKE) -C $(SPI_FLASH_SIM_DIR) clean
rm -f $(OBJ_FILES) $(TEST_OBJ_FILES) $(TEST_PROGRAM) $(COMPONENT_LIB) partition_table.bin
rm -f $(OBJ_FILES) $(TEST_OBJ_FILES) $(TEST_PROGRAM) $(COMPONENT_LIB) partition_table.bin image.bin
lib: $(BUILD_DIR)/$(COMPONENT_LIB)
@ -86,7 +86,11 @@ TEST_OBJ_FILES = $(filter %.o, $(TEST_SOURCE_FILES:.cpp=.o) $(TEST_SOURCE_FILES:
$(TEST_PROGRAM): lib $(TEST_OBJ_FILES) $(SPI_FLASH_SIM_BUILD_DIR)/$(SPI_FLASH_SIM_LIB) $(STUBS_LIB_BUILD_DIR)/$(STUBS_LIB) partition_table.bin $(SDKCONFIG)
g++ $(LDFLAGS) $(CXXFLAGS) -o $@ $(TEST_OBJ_FILES) -L$(BUILD_DIR) -l:$(COMPONENT_LIB) -L$(SPI_FLASH_SIM_BUILD_DIR) -l:$(SPI_FLASH_SIM_LIB) -L$(STUBS_LIB_BUILD_DIR) -l:$(STUBS_LIB)
test: $(TEST_PROGRAM)
# Use spiffs source directory as the test image
spiffs_image: ../spiffs $(shell find ../spiffs -type d) $(shell find ../spiffs -type -f -name '*')
../spiffsgen.py 2097152 ../spiffs image.bin
test: $(TEST_PROGRAM) spiffs_image
./$(TEST_PROGRAM)
# Create other necessary targets

View file

@ -1,6 +1,9 @@
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <dirent.h>
#include <limits.h>
#include "esp_partition.h"
#include "spiffs.h"
@ -11,19 +14,18 @@
extern "C" void init_spi_flash(const char* chip_size, size_t block_size, size_t sector_size, size_t page_size, const char* partition_bin);
TEST_CASE("format disk, open file, write and read file", "[spiffs]")
static void init_spiffs(spiffs *fs, uint32_t max_files)
{
init_spi_flash(CONFIG_ESPTOOLPY_FLASHSIZE, CONFIG_WL_SECTOR_SIZE * 16, CONFIG_WL_SECTOR_SIZE, CONFIG_WL_SECTOR_SIZE, "partition_table.bin");
spiffs fs;
spiffs_config cfg;
s32_t spiffs_res;
const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "storage");
REQUIRE(partition);
// Configure objects needed by SPIFFS
esp_spiffs_t esp_user_data;
esp_user_data.partition = partition;
fs.user_data = (void*)&esp_user_data;
esp_spiffs_t *user_data = (esp_spiffs_t*) calloc(1, sizeof(*user_data));
user_data->partition = partition;
fs->user_data = (void*)user_data;
cfg.hal_erase_f = spiffs_api_erase;
cfg.hal_read_f = spiffs_api_read;
@ -34,34 +36,131 @@ TEST_CASE("format disk, open file, write and read file", "[spiffs]")
cfg.phys_erase_block = CONFIG_WL_SECTOR_SIZE;
cfg.phys_size = partition->size;
uint32_t max_files = 5;
uint32_t work_sz = cfg.log_page_size * 2;
uint8_t *work = (uint8_t*) malloc(work_sz);
uint32_t fds_sz = max_files * sizeof(spiffs_fd);
uint32_t work_sz = cfg.log_page_size * 2;
uint8_t *fds = (uint8_t*) malloc(fds_sz);
#if CONFIG_SPIFFS_CACHE
uint32_t cache_sz = sizeof(spiffs_cache) + max_files * (sizeof(spiffs_cache_page)
+ cfg.log_page_size);
uint8_t *work = (uint8_t*) malloc(work_sz);
uint8_t *fds = (uint8_t*) malloc(fds_sz);
uint8_t *cache = (uint8_t*) malloc(cache_sz);
s32_t spiffs_res;
uint8_t *cache = (uint8_t*) malloc(cache_sz);
#else
uint32_t cache_sz = 0;
uint8_t cache = NULL;
#endif
// Special mounting procedure: mount, format, mount as per
// https://github.com/pellepl/spiffs/wiki/Using-spiffs
spiffs_res = SPIFFS_mount(&fs, &cfg, work, fds, fds_sz,
spiffs_res = SPIFFS_mount(fs, &cfg, work, fds, fds_sz,
cache, cache_sz, spiffs_api_check);
REQUIRE(spiffs_res == SPIFFS_ERR_NOT_A_FS);
spiffs_res = SPIFFS_format(&fs);
REQUIRE(spiffs_res >= SPIFFS_OK);
if (spiffs_res == SPIFFS_ERR_NOT_A_FS) {
spiffs_res = SPIFFS_format(fs);
REQUIRE(spiffs_res >= SPIFFS_OK);
spiffs_res = SPIFFS_mount(fs, &cfg, work, fds, fds_sz,
cache, cache_sz, spiffs_api_check);
}
spiffs_res = SPIFFS_mount(&fs, &cfg, work, fds, fds_sz,
cache, cache_sz, spiffs_api_check);
REQUIRE(spiffs_res >= SPIFFS_OK);
}
static void deinit_spiffs(spiffs *fs)
{
SPIFFS_unmount(fs);
free(fs->work);
free(fs->user_data);
free(fs->fd_space);
#if CONFIG_SPIFFS_CACHE
free(fs->cache);
#endif
}
static void check_spiffs_files(spiffs *fs, const char *base_path, char* cur_path)
{
DIR *dir;
struct dirent *entry;
size_t len = strlen(cur_path);
if (len == 0) {
strcpy(cur_path, base_path);
len = strlen(base_path);
}
dir = opendir(cur_path);
REQUIRE(dir != 0);
while ((entry = readdir(dir)) != NULL) {
char *name = entry->d_name;
if (entry->d_type == DT_DIR) {
if (!strcmp(name, ".") || !strcmp(name, ".."))
continue;
cur_path[len] = '/';
strcpy(cur_path + len + 1, name);
check_spiffs_files(fs, base_path, cur_path);
cur_path[len] = '\0';
} else {
char path[PATH_MAX];
// Read the file from host FS
strcpy(path, cur_path);
strcat(path, "/");
strcat(path, name);
FILE* f = fopen(path , "r");
REQUIRE(f);
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
char *f_contents = (char*) malloc(sz);
fread(f_contents, 1, sz, f);
fclose(f);
s32_t spiffs_res;
// Read the file from SPIFFS
char *spiffs_path = path + strlen(base_path);
spiffs_res = SPIFFS_open(fs, spiffs_path, SPIFFS_RDONLY, 0);
REQUIRE(spiffs_res > SPIFFS_OK);
spiffs_file fd = spiffs_res;
spiffs_stat stat;
spiffs_res = SPIFFS_stat(fs, spiffs_path, &stat);
char *spiffs_f_contents = (char*) malloc(stat.size);
spiffs_res = SPIFFS_read(fs, fd, spiffs_f_contents, stat.size);
REQUIRE(spiffs_res == stat.size);
// Compare the contents
REQUIRE(sz == stat.size);
bool same = memcmp(f_contents, spiffs_f_contents, sz) == 0;
REQUIRE(same);
free(f_contents);
free(spiffs_f_contents);
}
}
closedir(dir);
}
TEST_CASE("format disk, open file, write and read file", "[spiffs]")
{
init_spi_flash(CONFIG_ESPTOOLPY_FLASHSIZE, CONFIG_WL_SECTOR_SIZE * 16, CONFIG_WL_SECTOR_SIZE, CONFIG_WL_SECTOR_SIZE, "partition_table.bin");
spiffs fs;
s32_t spiffs_res;
init_spiffs(&fs, 5);
// Open test file
spiffs_res = SPIFFS_open(&fs, "test.txt", SPIFFS_O_CREAT | SPIFFS_O_RDWR, 0);
spiffs_res = SPIFFS_open(&fs, "test.txt", SPIFFS_O_CREAT | SPIFFS_O_RDWR, 0);
REQUIRE(spiffs_res >= SPIFFS_OK);
// Generate data
@ -77,17 +176,15 @@ TEST_CASE("format disk, open file, write and read file", "[spiffs]")
*((uint32_t*)(data + i)) = i;
}
s32_t bw;
// Write data to file
spiffs_res = SPIFFS_write(&fs, file, (void*)data, data_size);
spiffs_res = SPIFFS_write(&fs, file, (void*)data, data_size);
REQUIRE(spiffs_res >= SPIFFS_OK);
REQUIRE(spiffs_res == data_size);
// Set the file object pointer to the beginning
spiffs_res = SPIFFS_lseek(&fs, file, 0, SPIFFS_SEEK_SET);
REQUIRE(spiffs_res >= SPIFFS_OK);
// Read the file
spiffs_res = SPIFFS_read(&fs, file, (void*)read, data_size);
REQUIRE(spiffs_res >= SPIFFS_OK);
@ -99,9 +196,53 @@ TEST_CASE("format disk, open file, write and read file", "[spiffs]")
REQUIRE(memcmp(data, read, data_size) == 0);
// Unmount
SPIFFS_unmount(&fs);
deinit_spiffs(&fs);
free(read);
free(data);
}
TEST_CASE("can read spiffs image", "[spiffs]")
{
init_spi_flash(CONFIG_ESPTOOLPY_FLASHSIZE, CONFIG_WL_SECTOR_SIZE * 16, CONFIG_WL_SECTOR_SIZE, CONFIG_WL_SECTOR_SIZE, "partition_table.bin");
spiffs fs;
s32_t spiffs_res;
const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "storage");
// Write the contents of the image file to partition
FILE* img_file = fopen("image.bin", "r");
REQUIRE(img_file);
fseek(img_file, 0, SEEK_END);
long img_size = ftell(img_file);
fseek(img_file, 0, SEEK_SET);
char *img = (char*) malloc(img_size);
fread(img, 1, img_size, img_file);
fclose(img_file);
REQUIRE(partition->size == img_size);
esp_partition_erase_range(partition, 0, partition->size);
esp_partition_write(partition, 0, img, img_size);
free(img);
// Mount the spiffs partition and init filesystem, using the contents of
// the image file
init_spiffs(&fs, 1024);
// Check spiffs consistency
spiffs_res = SPIFFS_check(&fs);
REQUIRE(spiffs_res == SPIFFS_OK);
char path_buf[PATH_MAX];
// The image is created from the spiffs source directory. Compare the files in that
// directory to the files read from the SPIFFS image.
check_spiffs_files(&fs, "../spiffs", path_buf);
deinit_spiffs(&fs);
}

View file

@ -10,15 +10,74 @@ It supports wear leveling, file system consistency checks and more.
Notes
-----
- Presently, spiffs does not support directories. It produces a flat structure. If SPIFFS is mounted under ``/spiffs`` creating a file with path ``/spiffs/tmp/myfile.txt`` will create a file called ``/tmp/myfile.txt`` in SPIFFS, instead of ``myfile.txt`` under directory ``/spiffs/tmp``.
- Currently, SPIFFS does not support directories. It produces a flat structure. If SPIFFS is mounted under ``/spiffs``, then creating a file with path ``/spiffs/tmp/myfile.txt`` will create a file called ``/tmp/myfile.txt`` in SPIFFS, instead of ``myfile.txt`` under directory ``/spiffs/tmp``.
- It is not a realtime stack. One write operation might last much longer than another.
- Presently, it does not detect or handle bad blocks.
- Currently, it does not detect or handle bad blocks.
Tools
-----
Host-Side tools for creating SPIFS partition images exist and one such tool is `mkspiffs <https://github.com/igrr/mkspiffs>`_.
You can use it to create image from a given folder and then flash that image with ``esptool.py``
spiffsgen.py
^^^^^^^^^^^^
:component_file:`spiffsgen.py<spiffs/spiffsgen.py>` is a write-only Python SPIFFS implementation used to create filesystem
images from the contents of a host folder. To use ``spiffsgen.py``, simply invoke it from your favorite terminal::
python spiffsgen.py <image_size> <base_dir> <output_file>
- image_size: size of the partition on which the created SPIFFS image will be flashed to
- base_dir: directory to create the SPIFFS image of
- output_file: SPIFFS image output file
Besides the three required arguments: *image_size*, *base_dir* and *output_file*, there are other arguments
that control image generation. Documentation on these arguments exist in the tool's help::
python spiffsgen.py --help
These optional arguments correspond to possible SPIFFS build configuration.
User should make sure that the image is generated with the same arguments/configuration as
SPIFFS was built with, else the user ends up with an invalid image. As a guide, the help output indicates the SPIFFS
build configuration the argument corresponds to. In cases when these arguments
are not specified, the default values shown in the help output are used.
Once the image has been created, it can be flashed using ``esptool.py`` or ``parttool.py``.
Aside from invoking ``spiffsgen.py`` standalone, it is also possible to use it directly from the build system by calling
``spiffs_create_partition_image``.
Make::
$(eval $(call spiffs_create_partition_image,<partition>,<base_dir>,[FLASH_IN_PROJECT]))
CMake::
spiffs_create_partition_image(<partition> <base_dir> [FLASH_IN_PROJECT])
This is more convenient as the build configuration is automatically passed to the tool,
ensuring that the image generated is valid for that build. An example of this is while the *image_size* is required
for the standalone invocation, only the *partition* name is required when using ``spiffs_create_partition_image`` --
the image size is automatically obtained from the project's partition table.
It is important to note that due to the differences in structure between the two build systems,
when using Make, ``spiffs_create_partition_image`` must be called from the project Makefile;
for CMake, it should be called from one of the component CMakeLists.txt. For both build systems, the image will be created in the build directory
with filename *partition*.bin.
Optionally, user can opt to have the image automatically flashed together with the app binaries, partition tables, etc. on
``idf.py flash`` or ``make flash`` by specifying ``FLASH_IN_PROJECT``. For example::
spiffs_create_partition_image(my_spiffs_partition my_folder FLASH_IN_PROJECT)
If FLASH_IN_PROJECT is not specified, the image is still generated,
but user has to flash it manually using ``esptool.py``, ``parttool.py`` or a custom build system target.
For an example, see :example:`examples/build_system/spiffsgen>`.
mkspiffs
^^^^^^^^
Another tool for creating SPIFS partition images is `mkspiffs <https://github.com/igrr/mkspiffs>`_.
Like ``spiffsgen.py``, it can be used to create image from a given folder and then flash that image with ``esptool.py``
To do that you need to obtain some parameters:
@ -35,6 +94,27 @@ To flash the image to ESP32 at offset 0x110000::
python esptool.py --chip esp32 --port [port] --baud [baud] write_flash -z 0x110000 spiffs.bin
Notes on which SPIFFS tool to use
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The two tools presented above offer very similar functionality. There are, however, reasons to prefer one
over the other depending on the use case.
If the intent is to simply generate a SPIFFS image during build, ``spiffsgen.py`` makes it very convenient
by providing functions/commands from the build system itself. This makes it easy to generate SPIFFS images
that match the build configuration and can be flashed together with the application.
Another case for choosing ``spiffsgen.py`` is when the host has no C/C++ compiler available, since ``mkspiffs``
requires compilation.
On the other hand, ``mkspiffs`` offers unpacking SPIFFS images in addition to image generation. This is not
possible with ``spiffsgen.py``, at least not yet. There might also be environments where a Python interpreter
is not available, but a host compiler is or a pre-compiled ``mkspiffs`` binary
can do the job. However, there is no build system integration for ``mkspiffs`` and the user has to
do the corresponding work: compiling ``mkspiffs`` during build (if a pre-compiled binary is not used), creating build rules/targets
for the output files, passing proper parameters to the tool, etc.
See also
--------

View file

@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(spiffsgen)

View file

@ -0,0 +1,15 @@
#
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
# project subdirectory.
#
PROJECT_NAME := spiffsgen
include $(IDF_PATH)/make/project.mk
# Create a SPIFFS image from the contents of the 'spiffs_image' directory
# that fits the partition named 'storage'. FLASH_IN_PROJECT indicates that
# the generated image should be flashed when the entire project is flashed to
# the target with 'make flash'.
$(eval $(call spiffs_create_partition_image,storage,spiffs_image,FLASH_IN_PROJECT))

View file

@ -0,0 +1,59 @@
# SPIFFS Image Generation on Build Example
(See the README.md file in the upper level 'examples' directory for more information about examples.)
This example demonstrates how to use the SPIFFS image generation tool [spiffsgen.py](../../../components/spiffs/spiffsgen.py) to automatically create a SPIFFS
filesystem image from the contents of a host folder during build, with an option of
automatically flashing the created image on invocation of `idf.py flash` or `make flash`.
For more information, see description of `spiffsgen.py` on the ESP-IDF Programming Guide under API Reference > Storage > SPIFFS Filesystem.
The following gives an overview of the example:
1. There is a directory `spiffs_image` from which the SPIFFS filesystem image will be created.
2. The function `spiffs_create_partition_image` is used to specify that a SPIFFS image
should be created during build for the `storage` partition. For CMake, it is called from [the main component's CMakeLists.txt](./main/CMakeLists.txt);
for Make, from the [project Makefile](./Makefile). `FLASH_IN_PROJECT` specifies that the created image
should be flashed on invocation of `idf.py flash` or `make flash` together with app, bootloader, partition table, etc.
For both build systems, the image is created on the example's build directory with the output filename `storage.bin`.
3. Upon invocation of `idf.py flash monitor` or `make flash monitor`, application loads and
finds there is already a valid SPIFFS filesystem in the `storage` partition with files same as those in `spiffs_image` directory. The application is then
able to read those files.
## How to use example
### Build and flash
To run the example, type the following command:
```Makefile
# Make
make flash monitor
```
or
```CMake
# CMake
idf.py flash monitor
```
(To exit the serial monitor, type ``Ctrl-]``.)
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
## Example output
Here is the example's console output:
```
...
I (10) example: Initializing SPIFFS
I (110) example: Partition size: total: 896321, used: 171935
I (110) example: Reading hello.txt
I (110) example: Read from hello.txt: Hello World!
I (110) example: Computing alice.txt MD5 hash
I (330) example: Computed MD5 hash of alice.txt: deeb71f585cbb3ae5f7976d5127faf2a
I (330) example: SPIFFS unmounted
```
The logic of the example is contained in a [single source file](./main/spiffsgen_example_main.c), and it should be relatively simple to match points in its execution with the log outputs above.

View file

@ -0,0 +1,34 @@
from __future__ import print_function
import os
import sys
import hashlib
try:
import IDF
except ImportError:
test_fw_path = os.getenv('TEST_FW_PATH')
if test_fw_path and test_fw_path not in sys.path:
sys.path.insert(0, test_fw_path)
import IDF
@IDF.idf_example_test(env_tag='Example_WIFI')
def test_examples_spiffsgen(env, extra_data):
# Test with default build configurations
dut = env.get_dut('spiffsgen', 'examples/build_system/spiffsgen')
dut.start_app()
base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'spiffs_image')
# Expect hello.txt is read successfully
with open(os.path.join(base_dir, 'hello.txt'), 'r') as hello_txt:
dut.expect('Read from hello.txt: ' + hello_txt.read())
# Expect alice.txt MD5 hash is computed accurately
with open(os.path.join(base_dir, 'sub', 'alice.txt'), 'rb') as alice_txt:
alice_md5 = hashlib.md5(alice_txt.read()).hexdigest()
dut.expect('Computed MD5 hash of alice.txt: ' + alice_md5)
if __name__ == '__main__':
test_examples_spiffsgen()

View file

@ -0,0 +1,10 @@
set(COMPONENT_SRCS "spiffsgen_example_main.c")
set(COMPONENT_ADD_INCLUDEDIRS ".")
register_component()
# Create a SPIFFS image from the contents of the 'spiffs_image' directory
# that fits the partition named 'storage'. FLASH_IN_PROJECT indicates that
# the generated image should be flashed when the entire project is flashed to
# the target with 'idf.py flash'.
spiffs_create_partition_image(storage ../spiffs_image FLASH_IN_PROJECT)

View file

@ -0,0 +1,4 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)

View file

@ -0,0 +1,130 @@
/* SPIFFS Image Generation on Build Example
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "esp_err.h"
#include "esp_log.h"
#include "esp_spiffs.h"
#include "esp32/rom/md5_hash.h"
static const char *TAG = "example";
static void read_hello_txt()
{
ESP_LOGI(TAG, "Reading hello.txt");
// Open for reading hello.txt
FILE* f = fopen("/spiffs/hello.txt", "r");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open hello.txt");
return;
}
char buf[64];
memset(buf, 0, sizeof(buf));
fread(buf, 1, sizeof(buf), f);
fclose(f);
// Display the read contents from the file
ESP_LOGI(TAG, "Read from hello.txt: %s", buf);
}
static void compute_alice_txt_md5()
{
ESP_LOGI(TAG, "Computing alice.txt MD5 hash");
// The file alice.txt lives under a subdirectory, though SPIFFS itself is flat
FILE* f = fopen("/spiffs/sub/alice.txt", "r");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open alice.txt");
return;
}
// Read file and compute the digest chunk by chunk
#define MD5_MAX_LEN 16
char buf[64];
struct MD5Context ctx;
unsigned char digest[MD5_MAX_LEN];
MD5Init(&ctx);
size_t read;
do {
read = fread((void*) buf, 1, sizeof(buf), f);
MD5Update(&ctx, (unsigned const char*) buf, read);
} while(read == sizeof(buf));
MD5Final(digest, &ctx);
// Create a string of the digest
char digest_str[MD5_MAX_LEN * 2];
for (int i = 0; i < MD5_MAX_LEN; i++) {
sprintf(&digest_str[i * 2], "%02x", (unsigned int)digest[i]);
}
// For reference, MD5 should be deeb71f585cbb3ae5f7976d5127faf2a
ESP_LOGI(TAG, "Computed MD5 hash of alice.txt: %s", digest_str);
fclose(f);
}
void app_main(void)
{
ESP_LOGI(TAG, "Initializing SPIFFS");
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = NULL,
.max_files = 5,
.format_if_mount_failed = false
};
// Use settings defined above to initialize and mount SPIFFS filesystem.
// Note: esp_vfs_spiffs_register is an all-in-one convenience function.
esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount or format filesystem");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to find SPIFFS partition");
} else {
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
}
return;
}
size_t total = 0, used = 0;
ret = esp_spiffs_info(NULL, &total, &used);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}
/* The following calls demonstrate reading files from the generated SPIFFS
* image. The images should contain the same files and contents as the spiffs_image directory.
*/
// Read and display the contents of a small text file (hello.txt)
read_hello_txt();
// Compute and display the MD5 hash of a large text file (alice.txt)
compute_alice_txt_md5();
// All done, unmount partition and disable SPIFFS
esp_vfs_spiffs_unregister(NULL);
ESP_LOGI(TAG, "SPIFFS unmounted");
}

View file

@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
storage, data, spiffs, , 0xF0000,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 1M,
6 storage, data, spiffs, , 0xF0000,

View file

@ -0,0 +1,5 @@
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv"
CONFIG_PARTITION_TABLE_CUSTOM_APP_BIN_OFFSET=0x10000
CONFIG_PARTITION_TABLE_FILENAME="partitions_example.csv"
CONFIG_APP_OFFSET=0x10000

View file

@ -0,0 +1 @@
Hello World!

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@ components/app_update/gen_empty_partition.py
components/app_update/otatool.py
components/partition_table/test_gen_esp32part_host/gen_esp32part_tests.py
components/ulp/esp32ulp_mapgen.py
components/spiffs/spiffsgen.py
docs/check_doc_warnings.sh
docs/check_lang_folder_sync.sh
docs/gen-kconfig-doc.py