Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions platformio/builder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from SCons.Script import Default # pylint: disable=import-error
from SCons.Script import DefaultEnvironment # pylint: disable=import-error
from SCons.Script import Import # pylint: disable=import-error
from SCons.Script import SetOption # pylint: disable=import-error
from SCons.Script import Variables # pylint: disable=import-error

from platformio import app, fs
Expand All @@ -35,6 +36,13 @@

AllowSubstExceptions(NameError)

# SCons normally reuses stored content signatures for files older than its
# max-drift window when their size and modification time are unchanged. A VCS
# checkout can replace a source file while preserving both values, which makes
# the previous object look current. Always calculate dependency signatures from
# the actual contents so incremental builds cannot mix source states.
SetOption("max_drift", -1)

# append CLI arguments to build environment
clivars = Variables(None)
clivars.AddVariables(
Expand Down
49 changes: 49 additions & 0 deletions tests/commands/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,60 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import subprocess
import time
from pathlib import Path

from platformio.run.cli import cli as cmd_run


def test_rebuilds_source_when_content_changes_without_metadata_change(
clirunner, validate_cliresult, tmp_path: Path
):
project_dir = tmp_path / "project"
src_dir = project_dir / "src"
src_dir.mkdir(parents=True)
(project_dir / "platformio.ini").write_text("""
[env:native]
platform = native
""")

source_path = src_dir / "main.c"
source_path.write_text("int main(void) { return 0; }\n")
old_timestamp = time.time() - (3 * 24 * 60 * 60)
os.utime(source_path, (old_timestamp, old_timestamp))
original_stat = source_path.stat()

result = clirunner.invoke(cmd_run, ["--project-dir", str(project_dir)])
validate_cliresult(result)

# Reproduce a Git checkout that restores different content with the same
# file size and modification time. Timestamp-based SCons deciders otherwise
# risk reusing the object produced from the previous source state.
source_path.write_text("int main(void) { return 1; }\n")
os.utime(
source_path,
ns=(original_stat.st_atime_ns, original_stat.st_mtime_ns),
)

result = clirunner.invoke(cmd_run, ["--project-dir", str(project_dir)])
validate_cliresult(result)

object_path = project_dir / ".pio" / "build" / "native" / "src" / "main.o"
object_mtime_ns = object_path.stat().st_mtime_ns

program_name = "program.exe" if os.name == "nt" else "program"
program_path = project_dir / ".pio" / "build" / "native" / program_name
assert subprocess.run([program_path], check=False).returncode == 1

# Hashing the source again must not turn an unchanged build into a rebuild.
time.sleep(0.01)
result = clirunner.invoke(cmd_run, ["--project-dir", str(project_dir)])
validate_cliresult(result)
assert object_path.stat().st_mtime_ns == object_mtime_ns


def test_generic_build(clirunner, validate_cliresult, tmpdir):
build_flags = [
("-D TEST_INT=13", "-DTEST_INT=13"),
Expand Down