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
79 changes: 54 additions & 25 deletions .git-hooks/pre-push.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,36 @@
import subprocess
import sys
import tempfile
from pathlib import Path

import edgutil
# Git invokes this hook with the ambient environment, which does not
# necessarily have PYTHONPATH configured (e.g., on Windows). Locate
# "dev_tools/pylibs" relative to this script so "edgutil" can be imported.
sys.path.insert(
0, str(Path(__file__).resolve().parents[1] / 'dev_tools' / 'pylibs')
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love this, I'd prefer people actual configure the environment (which dev-init.py should help with ... which also installs the script?). However, if we're only doing this for the pre-push.py script (as a sort of "balancing act" to handle weird edge cases), I think that's fine.

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood: it is deliberately only in pre-push.py, as the balancing act you describe. Every other tool is invoked from a configured environment, but git runs this hook with the ambient environment, and nothing surfaced that distinction to the user today (the failure was an ImportError inside a closing console window). And yes, dev-init.py installs the hook (it writes the .git/hooks/pre-push shim that executes .git-hooks/pre-push.py), so dev-init users get the self-locating version automatically.


from pathlib import Path
import edgutil

if platform.system() == 'Windows':
# On Windows we need to use the kernel API to detect if this script is
# running in a standalone console window (if so, there will be only one
# processes at this point).
import ctypes

kernel32 = ctypes.WinDLL('kernel32', use_last_error = True)
process_array = (ctypes.c_uint * 2)()
num_processes = kernel32.GetConsoleProcessList(process_array, 2)
if num_processes != 1:
# When not running in a standalone console window, the script relaunches
# itself to make sure the user has an interactive shell. Then it forwards
# the exit code from the delegate process.
# The standard input of this hook carries the push ref list supplied by
# git, so it cannot be used to prompt the user. When a console is
# attached, open the console input device directly ("CONIN$" is the
# Windows counterpart of "/dev/tty") so that all prompts and output stay
# in the user's current terminal.
try:
sys.stdin = open('CONIN$', 'r')
except OSError:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confession: I wrote the original logic here, I'm not a "Windows guy" but it seemed to work and be reasonably correct; I'll defer to others that know Windows better as to if this is an improvement.

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context! The Windows rationale: CONIN$ is the console input device and works like /dev/tty: it can be opened regardless of what stdin is connected to (git feeds the hook the ref list on stdin, so stdin itself can't serve the prompts). With a console attached (cmd, PowerShell, Windows Terminal), everything now stays in the user's current terminal. Your relaunch is preserved as the fallback for the cases where CONIN$ cannot open (GUI git clients, mintty-style pseudo-terminals), so that intent is kept; that fallback window also now waits for Enter on failure so its output can't flash away.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd and PowerShell are shells, and Windows Terminal is a terminal GUI. From the Windows perspective, none of them are consoles; only conhost.exe and openconsole.exe are.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For performance, NtQueryVolumeInformationFile should be used, but since this is a Python script, the current approach is acceptable.

# No console is attached (e.g., a GUI git client or a pseudo terminal
# such as mintty). Relaunch in a standalone console window so that the
# user can still respond to interactive prompts.
delegate_proc = subprocess.run(
[
sys.executable,
__file__
],
creationflags = subprocess.CREATE_NEW_CONSOLE
creationflags = subprocess.CREATE_NEW_CONSOLE,
env = {**os.environ, 'EDG_PREPUSH_DELEGATE': '1'}
)
sys.exit(delegate_proc.returncode)
else:
Expand Down Expand Up @@ -137,6 +143,7 @@ class PrePushChecker:

def __init__(self):
self.errors = 0
self.abort = False

def confirm_okay(self):
'''Ask the user whether a policy warning is acceptable.
Expand All @@ -159,7 +166,8 @@ def confirm_okay(self):
return
if response == 'q':
self.errors = 1
sys.exit(1)
self.abort = True
return

print(
"Please answer 'y' (to accept), 'n' (to continue check) or "
Expand All @@ -168,15 +176,18 @@ def confirm_okay(self):
tries += 1
if tries >= 10:
print('Too many attempts.')
sys.exit(1)
self.errors = 1
self.abort = True
return

def check_commits(self):
'''Walk commits until one already on a remote branch is found.

As commits are walked, check to make sure they match formatting guidance.

Returns the oldest new commit hash, or None if there are no new commits.
Exits immediately if a commit subject fails the PR-number policy.
Returns the oldest new commit hash, or None if there are no new commits
(or if a commit subject fails the PR-number policy, in which case an
error is recorded and the check sequence is aborted).
'''
first_new_commit = None

Expand All @@ -200,7 +211,9 @@ def check_commits(self):
print(
" [POLICY] Commit message doesn't contain valid PR number (or [])."
)
sys.exit(1)
self.errors = 1
self.abort = True
return None

first_new_commit = commit

Expand Down Expand Up @@ -285,6 +298,8 @@ def check_changed_files(self, first_new_commit):
results = json.load(file_handle)

for file_path_str, file_results in results.items():
if self.abort:
break
print(f" Checking file: {file_path_str}")

misspellings = file_results['misspellings']
Expand Down Expand Up @@ -336,18 +351,32 @@ def check_changed_files(self, first_new_commit):
print('=' * 79)
self.confirm_okay()

def run(self) -> bool:
'''Execute the full pre-push check sequence.'''
def run(self) -> int:
'''Execute the full pre-push check sequence. Return the exit status.'''
first_new_commit = self.check_commits()

# Skip per file checks if we didn't actually detect a new commit.
if first_new_commit is not None:
if not self.abort and first_new_commit is not None:
self.check_changed_files(first_new_commit)

return self.errors == 0
return 0 if (self.errors == 0 and not self.abort) else 1

def pause_delegate_window() -> None:
'''When running as a relaunch delegate, wait for the user before the
standalone console window closes so that failure output can be read.'''
if os.environ.get('EDG_PREPUSH_DELEGATE') != '1':
return

try:
input('Push checks failed; press Enter to close this window. ')
except EOFError:
pass

def main():
checker = PrePushChecker()
sys.exit(0 if checker.run() else 1)
returncode = checker.run()
if returncode != 0:
pause_delegate_window()
sys.exit(returncode)

main()
22 changes: 22 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ If direnv is not an option, manually add `<EDG PROJECT DIR>/dev_tools/bin` (or
variables respectively. This will make the EDG development tools and their
supporting Python libraries available.

For Windows users, a PowerShell counterpart of the direnv setup is provided
at the root of the repository; from a PowerShell prompt dot source it to
configure the current session:

```
. .\dev-env.ps1
```

(If PowerShell script execution is disabled, first run
`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`, or use the manual
environment variable configuration described above.)

> [!WARNING]
>
> Windows users should ensure their clone uses LF line endings, e.g., by
> setting `git config core.autocrlf input` before cloning (an existing clone
> can be repaired by setting that configuration and re-checking out the
> tree). The default Windows Git configuration (`core.autocrlf true`)
> produces a CRLF checkout, which breaks both the build (e.g., `mk_errinfo`
> cannot parse `src/error_msg.txt`) and the Docker based workflow (scripts
> mounted into the container fail on CRLF shebangs).

## Docker Based (Recommended)

To get started with Docker, it is recommended that Windows and MacOS users
Expand Down
40 changes: 40 additions & 0 deletions dev-env.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Part of the EDG Compiler Project, under the Apache License v2.0 with LLVM
# Exceptions.
# See https://edgcpp.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

# Windows counterpart of the ".envrc" file used with direnv on POSIX systems:
# from a PowerShell prompt, dot source this script to make the EDG
# development tools and their supporting Python libraries available in the
# current session:
#
# . .\dev-env.ps1
#
# This prepends "dev_tools/win-bin" and "dev_tools/bin" to the PATH and
# "dev_tools/pylibs" to the PYTHONPATH. The script may be dot sourced from
# any directory and repeated dot sourcing is harmless.

$edgRepoRoot = $PSScriptRoot

$edgToolDirs = @(
(Join-Path $edgRepoRoot 'dev_tools\win-bin'),
(Join-Path $edgRepoRoot 'dev_tools\bin')
)

if (-not (@($env:Path -split ';' |
Where-Object { $edgToolDirs -icontains $_ }))) {
$env:Path = "$($edgToolDirs -join ';');$env:Path"
}

$edgPyLibs = Join-Path $edgRepoRoot 'dev_tools\pylibs'
if (-not (@($env:PYTHONPATH -split ';' |
Where-Object { $_ -ieq $edgPyLibs }))) {
if ($env:PYTHONPATH) {
$env:PYTHONPATH = "$edgPyLibs;$env:PYTHONPATH"
} else {
$env:PYTHONPATH = $edgPyLibs
}
}

Remove-Variable -Name edgRepoRoot, edgToolDirs, edgPyLibs `
-ErrorAction SilentlyContinue

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat!

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! :)

39 changes: 37 additions & 2 deletions dev-init.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,25 @@ async def check_edg_python() -> InitResult:

if platform.system() == 'Windows':
find_edgy.add_failure_commentary(
'Open the Windows environment variable settings:'
'For the current PowerShell session only, run (from the repository '
'root):'
)
find_edgy.add_failure_commentary(
' . .\\dev-env.ps1',
allow_wrap = False
)
find_edgy.add_failure_commentary(
'and re-run this script; or, to configure new terminals '
'permanently, run:'
)
find_edgy.add_failure_commentary(
' [Environment]::SetEnvironmentVariable("Path", '
'[Environment]::GetEnvironmentVariable("Path", "User") '
f"+ ';{script_path}', 'User')",
allow_wrap = False
)
find_edgy.add_failure_commentary(
'Alternatively, open the Windows environment variable settings:'
)
find_edgy.add_failure_commentary(
' "C:\\Windows\\system32\\rundll32.exe" '
Expand Down Expand Up @@ -271,7 +289,24 @@ async def check_edg_python() -> InitResult:

if platform.system() == 'Windows':
find_edg_tools.add_failure_commentary(
'Open the Windows environment variable settings:'
'For the current PowerShell session only, run (from the repository '
'root):'
)
find_edg_tools.add_failure_commentary(
' . .\\dev-env.ps1',
allow_wrap = False
)
find_edg_tools.add_failure_commentary(
'and re-run this script; or, to configure new terminals '
'permanently, run:'
)
find_edg_tools.add_failure_commentary(
' [Environment]::SetEnvironmentVariable("PYTHONPATH", '
f"'{pylibs_path}', 'User')",
allow_wrap = False
)
find_edg_tools.add_failure_commentary(
'Alternatively, open the Windows environment variable settings:'
)
find_edg_tools.add_failure_commentary(
' "C:\\Windows\\system32\\rundll32.exe" '
Expand Down
14 changes: 11 additions & 3 deletions dev_tools/pylibs/edgdocker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,13 @@ def _check_parents(mapped_dirs: List[Path], workspace_dir: Path) -> None:
This can be valid (from a Docker perspective), but it's an advanced use case
and can be a dangerous mistake (so we don't permit it).
'''
workspace_drive = os.path.splitdrive(str(workspace_dir))[0]
for mapped_dir in mapped_dirs:
if os.path.splitdrive(str(mapped_dir))[0] != workspace_drive:
# The two paths are on different drives (which can only happen on
# Windows), so neither path can contain the other and the check below
# does not apply.
continue

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to do a bit more interrogation as this is a sort of safety check(?)

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Replaced the blanket except ValueError with an explicit check in 3d96ddb: the overlap check is now skipped only when the two paths are on genuinely different drives (os.path.splitdrive comparison), documented as "neither path can contain the other". Same-drive paths always get the full commonpath check, and any other unexpected condition now raises loudly instead of being silently skipped.

common_dir = Path(os.path.commonpath([mapped_dir, workspace_dir]))
if common_dir == workspace_dir:
print('Current workspace is the parent of a mapped dir:')
Expand Down Expand Up @@ -390,7 +396,7 @@ def exec_docker_cmd(image_name: str, workspace_dir: Path,
f"EDG_USER_NAME={render_user_name()}"
]

if os.isatty(sys.stdin.fileno()):
if edgutil.is_interactive_terminal():
# Make the container interactive.
docker_args.append('-it')
else:
Expand Down Expand Up @@ -473,13 +479,15 @@ def exec_docker_cmd(image_name: str, workspace_dir: Path,

if platform.system() == 'Windows':
# Windows does not properly handle the execvp. Instead, keep this process
# around, and simply forward on stdin, stdout, and stderr.
subprocess.run(
# around, and simply forward on stdin, stdout, and stderr. Forward the
# exit status of the container so that callers observe failures.
completed_proc = subprocess.run(
docker_args,
stdin = sys.stdin,
stdout = sys.stdout,
stderr = sys.stderr
)
sys.exit(completed_proc.returncode)
else:
# Execute the docker command, replacing the current process.
os.execvp('docker', docker_args)
Expand Down
23 changes: 23 additions & 0 deletions dev_tools/pylibs/edgutil/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,29 @@ def print_env_var(env_var: str) -> None:
var_value = os.environ.get(env_var) or ''
print(f"Using \"{env_var}\": \"{var_value}\"")

def is_interactive_terminal() -> bool:
'''Return TRUE if standard input is attached to an interactive terminal.

On Windows, "os.isatty" (and "sys.stdin.isatty") returns TRUE for some
character devices that are not consoles (e.g., "NUL", which is used when
a caller passes "subprocess.DEVNULL" as standard input), which would
incorrectly request TTY attachment for a container. A real console
handle can be identified by "GetConsoleMode", which fails for all other
devices.
'''
if platform.system() != 'Windows':
return os.isatty(sys.stdin.fileno())

import ctypes
import msvcrt

kernel32 = ctypes.WinDLL('kernel32', use_last_error = True)
kernel32.GetConsoleMode.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
kernel32.GetConsoleMode.restype = ctypes.c_int
stdin_handle = msvcrt.get_osfhandle(sys.stdin.fileno())
console_mode = ctypes.c_uint(0)
return bool(kernel32.GetConsoleMode(stdin_handle, ctypes.byref(console_mode)))

def get_application_files_dir(app_name: str) -> Path:
platform_name = platform.system()
if platform_name == 'Linux':
Expand Down
13 changes: 13 additions & 0 deletions doc/source/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ IFC file should be force-generated for (this is unusual). These module
source files are compiled under the ``--module_internal_partition`` flag to
generate both an IFC and a ``.obj`` file.

Note that when testing modules in a compiler emulation mode (e.g., with
``--gnu_version=150100`` added to ``//options_all:``), the ``--modules``
option must also be specified: emulation modes disable the module keywords
by default to match the emulated compiler (no version of g++ enables them
without ``-fmodules-ts``). For example:

.. code:: text

//options_all:--c++20 --gnu_version=150100 --modules

See the ``modules/eifc/modules/import-function-gnu`` and
``modules/eifc/modules/export-float16-gnu`` tests for examples.

If ``//script:`` is specified, that script is executed instead of any other
commands. In general, the test type does not matter for script files except
for checking the return code. For example, a ``cp`` test will expect a zero
Expand Down
13 changes: 13 additions & 0 deletions src/Changes
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
Version 7.1

10/16/26 [GH #303]
C++20 modules: gnu emulation modes and extended floating-point types

Module interface units compiled in a gnu or clang emulation mode always
failed with a catastrophic error indicating that an IFC file could not be
produced: the predeclared typedefs of extended floating-point types
(e.g., "_Float16") entered by those modes have no IFC encoding.
Predeclared typedefs are now skipped when the members of a scope are
written to an IFC file (they are re-created by any importing translation
unit), and the "_Float16", "_Float32", "_Float64", and "_Float128" type
families can now be written to and read from IFC files.


10/16/26 [GH #229]
Improper handling of "if constexpr" substatements

Expand Down
Loading