1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-01 19:02:18 +01:00
Files
esphome/esphome/writer.py
2025-07-26 20:54:03 +12:00

318 lines
9.5 KiB
Python

import importlib
import logging
import os
from pathlib import Path
import re
from esphome import loader
from esphome.config import iter_component_configs, iter_components
from esphome.const import (
HEADER_FILE_EXTENSIONS,
PLATFORM_ESP32,
SOURCE_FILE_EXTENSIONS,
__version__,
)
from esphome.core import CORE, EsphomeError
from esphome.helpers import (
copy_file_if_changed,
read_file,
walk_files,
write_file_if_changed,
)
from esphome.storage_json import StorageJSON, storage_path
_LOGGER = logging.getLogger(__name__)
CPP_AUTO_GENERATE_BEGIN = "// ========== AUTO GENERATED CODE BEGIN ==========="
CPP_AUTO_GENERATE_END = "// =========== AUTO GENERATED CODE END ============"
CPP_INCLUDE_BEGIN = "// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ==========="
CPP_INCLUDE_END = "// ========== AUTO GENERATED INCLUDE BLOCK END ==========="
CPP_BASE_FORMAT = (
"""// Auto generated code by esphome
""",
""""
void setup() {
""",
"""
App.setup();
}
void loop() {
App.loop();
}
""",
)
UPLOAD_SPEED_OVERRIDE = {
"esp210": 57600,
}
def get_flags(key):
flags = set()
for _, component, conf in iter_component_configs(CORE.config):
flags |= getattr(component, key)(conf)
return flags
def get_include_text():
include_text = '#include "esphome.h"\nusing namespace esphome;\n'
for _, component, conf in iter_component_configs(CORE.config):
if not hasattr(component, "includes"):
continue
includes = component.includes
if callable(includes):
includes = includes(conf)
if includes is None:
continue
if isinstance(includes, list):
includes = "\n".join(includes)
if not includes:
continue
include_text += f"{includes}\n"
return include_text
def replace_file_content(text, pattern, repl):
content_new, count = re.subn(pattern, repl, text, flags=re.MULTILINE)
return content_new, count
def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
if old is None:
return True
if old.src_version != new.src_version:
return True
return old.build_path != new.build_path
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
if (
old.loaded_integrations != new.loaded_integrations
or old.loaded_platforms != new.loaded_platforms
) and new.core_platform == PLATFORM_ESP32:
from esphome.components.esp32 import FRAMEWORK_ESP_IDF
return new.framework == FRAMEWORK_ESP_IDF
return False
def update_storage_json():
path = storage_path()
old = StorageJSON.load(path)
new = StorageJSON.from_esphome_core(CORE, old)
if old == new:
return
if storage_should_clean(old, new):
_LOGGER.info("Core config, version changed, cleaning build files...")
clean_build()
elif storage_should_update_cmake_cache(old, new):
_LOGGER.info("Integrations changed, cleaning cmake cache...")
clean_cmake_cache()
new.save(path)
def find_begin_end(text, begin_s, end_s):
begin_index = text.find(begin_s)
if begin_index == -1:
raise EsphomeError(
"Could not find auto generated code begin in file, either "
"delete the main sketch file or insert the comment again."
)
if text.find(begin_s, begin_index + 1) != -1:
raise EsphomeError(
"Found multiple auto generate code begins, don't know "
"which to chose, please remove one of them."
)
end_index = text.find(end_s)
if end_index == -1:
raise EsphomeError(
"Could not find auto generated code end in file, either "
"delete the main sketch file or insert the comment again."
)
if text.find(end_s, end_index + 1) != -1:
raise EsphomeError(
"Found multiple auto generate code endings, don't know "
"which to chose, please remove one of them."
)
return text[:begin_index], text[(end_index + len(end_s)) :]
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
#pragma once
#include "esphome/core/macros.h"
{}
"""
VERSION_H_FORMAT = """\
#pragma once
#include "esphome/core/macros.h"
#define ESPHOME_VERSION "{}"
#define ESPHOME_VERSION_CODE VERSION_CODE({}, {}, {})
"""
DEFINES_H_TARGET = "esphome/core/defines.h"
VERSION_H_TARGET = "esphome/core/version.h"
ESPHOME_README_TXT = """
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
ESPHome automatically populates the build directory, and any
changes to this directory will be removed the next time esphome is
run.
For modifying esphome's core files, please use a development esphome install,
the custom_components folder or the external_components feature.
"""
def copy_src_tree():
source_files: list[loader.FileResource] = []
for _, component in iter_components(CORE.config):
source_files += component.resources
source_files_map = {
Path(x.package.replace(".", "/") + "/" + x.resource): x for x in source_files
}
# Convert to list and sort
source_files_l = list(source_files_map.items())
source_files_l.sort()
# Build #include list for esphome.h
include_l = []
for target, _ in source_files_l:
if target.suffix in HEADER_FILE_EXTENSIONS:
include_l.append(f'#include "{target}"')
include_l.append("")
include_s = "\n".join(include_l)
source_files_copy = source_files_map.copy()
ignore_targets = [Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET)]
for t in ignore_targets:
source_files_copy.pop(t)
for fname in walk_files(CORE.relative_src_path("esphome")):
p = Path(fname)
if p.suffix not in SOURCE_FILE_EXTENSIONS:
# Not a source file, ignore
continue
# Transform path to target path name
target = p.relative_to(CORE.relative_src_path())
if target in ignore_targets:
# Ignore defines.h, will be dealt with later
continue
if target not in source_files_copy:
# Source file removed, delete target
p.unlink()
else:
src_file = source_files_copy.pop(target)
with src_file.path() as src_path:
copy_file_if_changed(src_path, p)
# Now copy new files
for target, src_file in source_files_copy.items():
dst_path = CORE.relative_src_path(*target.parts)
with src_file.path() as src_path:
copy_file_if_changed(src_path, dst_path)
# Finally copy defines
write_file_if_changed(
CORE.relative_src_path("esphome", "core", "defines.h"), generate_defines_h()
)
write_file_if_changed(CORE.relative_build_path("README.txt"), ESPHOME_README_TXT)
write_file_if_changed(
CORE.relative_src_path("esphome.h"), ESPHOME_H_FORMAT.format(include_s)
)
write_file_if_changed(
CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h()
)
platform = "esphome.components." + CORE.target_platform
try:
module = importlib.import_module(platform)
copy_files = getattr(module, "copy_files")
copy_files()
except AttributeError:
pass
def generate_defines_h():
define_content_l = [x.as_macro for x in CORE.defines]
define_content_l.sort()
return DEFINES_H_FORMAT.format("\n".join(define_content_l))
def generate_version_h():
match = re.match(r"^(\d+)\.(\d+).(\d+)-?\w*$", __version__)
if not match:
raise EsphomeError(f"Could not parse version {__version__}.")
return VERSION_H_FORMAT.format(
__version__, match.group(1), match.group(2), match.group(3)
)
def write_cpp(code_s):
path = CORE.relative_src_path("main.cpp")
if os.path.isfile(path):
text = read_file(path)
code_format = find_begin_end(
text, CPP_AUTO_GENERATE_BEGIN, CPP_AUTO_GENERATE_END
)
code_format_ = find_begin_end(
code_format[0], CPP_INCLUDE_BEGIN, CPP_INCLUDE_END
)
code_format = (code_format_[0], code_format_[1], code_format[1])
else:
code_format = CPP_BASE_FORMAT
copy_src_tree()
global_s = '#include "esphome.h"\n'
global_s += CORE.cpp_global_section
full_file = f"{code_format[0] + CPP_INCLUDE_BEGIN}\n{global_s}{CPP_INCLUDE_END}"
full_file += (
f"{code_format[1] + CPP_AUTO_GENERATE_BEGIN}\n{code_s}{CPP_AUTO_GENERATE_END}"
)
full_file += code_format[2]
write_file_if_changed(path, full_file)
def clean_cmake_cache():
pioenvs = CORE.relative_pioenvs_path()
if os.path.isdir(pioenvs):
pioenvs_cmake_path = CORE.relative_pioenvs_path(CORE.name, "CMakeCache.txt")
if os.path.isfile(pioenvs_cmake_path):
_LOGGER.info("Deleting %s", pioenvs_cmake_path)
os.remove(pioenvs_cmake_path)
def clean_build():
import shutil
pioenvs = CORE.relative_pioenvs_path()
if os.path.isdir(pioenvs):
_LOGGER.info("Deleting %s", pioenvs)
shutil.rmtree(pioenvs)
piolibdeps = CORE.relative_piolibdeps_path()
if os.path.isdir(piolibdeps):
_LOGGER.info("Deleting %s", piolibdeps)
shutil.rmtree(piolibdeps)
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
/secrets.yaml
"""
def write_gitignore():
path = CORE.relative_config_path(".gitignore")
if not os.path.isfile(path):
with open(file=path, mode="w", encoding="utf-8") as f:
f.write(GITIGNORE_CONTENT)