Skip to content

Commit 1360ee5

Browse files
committed
fix(nodeenv): append in front of the signature block of Activate.ps1
The Activate.ps1 python ships on Windows is signed, and PowerShell will not parse a script with code after its signature block: the whole activation failed with "Executable script code found in signature block" instead of only losing the prompt. writefile() now inserts an appended part in front of the block. The signature no longer matches, but editing the script voids it whichever end the new part goes to. The PowerShell probe runs with $ErrorActionPreference = 'Stop' and its stderr folded into stdout, so a script that cannot be parsed fails the test that runs it instead of the next one.
1 parent e80e2c0 commit 1360ee5

3 files changed

Lines changed: 51 additions & 7 deletions

File tree

‎nodeenv.py‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,9 @@ def make_executable(filename):
667667
os.chmod(filename, mode_0755)
668668

669669

670+
PS1_SIGNATURE = b'# SIG # Begin signature block'
671+
672+
670673
# noinspection PyArgumentList
671674
def writefile(dest, content, overwrite=True, append=False):
672675
"""
@@ -696,10 +699,21 @@ def writefile(dest, content, overwrite=True, append=False):
696699

697700
if append:
698701
logger.info(' * Appending data to %s', dest)
699-
with open(dest, 'ab') as f:
700-
if c and not c.endswith(b'\n'):
701-
f.write(b'\n')
702-
f.write(content)
702+
# PowerShell refuses to parse code that follows the signature
703+
# block of a signed script, and the Activate.ps1 python ships
704+
# on Windows is signed, so the new part goes in front of it.
705+
# Editing the script voids that signature either way
706+
# https://github.com/ekalinin/nodeenv/issues/243
707+
head, signature, rest = c.partition(PS1_SIGNATURE)
708+
# and the appended part starts on a line of its own: a
709+
# "deactivate.bat" ending with `:END` and no newline would
710+
# swallow the first appended line into the label
711+
if head and not head.endswith(b'\n'):
712+
head += b'\n'
713+
if signature and not content.endswith(b'\n'):
714+
content += b'\n'
715+
with open(dest, 'wb') as f:
716+
f.write(head + content + signature + rest)
703717
return
704718

705719
logger.info(' * Overwriting %s with new content', dest)

‎tests/nodeenv_test.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2597,3 +2597,23 @@ def test_append_starts_on_its_own_line(self, tmpdir):
25972597

25982598
assert dest.read() == (
25992599
'@echo off\n:END\n@echo off\nset NODE_VIRTUAL_ENV=\n')
2600+
2601+
def test_append_goes_before_a_powershell_signature(self, tmpdir):
2602+
"""
2603+
PowerShell refuses to parse code that follows the signature
2604+
block, and the Activate.ps1 python ships on Windows is signed
2605+
"""
2606+
dest = tmpdir.join('Activate.ps1')
2607+
dest.write('$env:VIRTUAL_ENV = "C:\\ws"\n'
2608+
'# SIG # Begin signature block\n'
2609+
'# MIIF...\n'
2610+
'# SIG # End signature block\n')
2611+
2612+
nodeenv.writefile(
2613+
str(dest), '$env:NODE_VIRTUAL_ENV = "C:\\ws"\n', append=True)
2614+
2615+
assert dest.read() == ('$env:VIRTUAL_ENV = "C:\\ws"\n'
2616+
'$env:NODE_VIRTUAL_ENV = "C:\\ws"\n'
2617+
'# SIG # Begin signature block\n'
2618+
'# MIIF...\n'
2619+
'# SIG # End signature block\n')

‎tests/test_activate_win.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,19 @@ def _parse(out, shell):
104104
out = out.replace('\r\n', '\n')
105105
_, sentinel, dump = out.partition(DUMP_SENTINEL + '\n')
106106
assert sentinel, 'no dump in the output of %s: %r' % (shell, out)
107-
return dict(line.split('=', 1) for line in dump.splitlines() if line)
107+
# anything the shell itself reported comes before the dump, but an
108+
# error message can still follow it
109+
return dict(line.split('=', 1) for line in dump.splitlines()
110+
if line.split('=', 1)[0] in PROBED)
108111

109112

110113
def _run(args, script, lines, shell):
111114
script.write('\n'.join(lines) + '\n')
115+
# stderr goes to stdout so a failing script reports why in the
116+
# assertion instead of leaving an empty dump behind
112117
out = subprocess.check_output(
113-
args + [str(script)], env=_child_env(), cwd=str(script.dirname))
118+
args + [str(script)], stderr=subprocess.STDOUT,
119+
env=_child_env(), cwd=str(script.dirname))
114120
return _parse(out.decode('utf-8'), shell)
115121

116122

@@ -122,7 +128,11 @@ def run_cmd(env, steps=()):
122128

123129
def run_ps1(env, steps=()):
124130
"""Run the steps in PowerShell and return the probed environment."""
125-
lines = list(steps) + [_dump_line(env.dumper, call='&')]
131+
# without this a script that cannot be parsed, which is what a
132+
# signed Activate.ps1 with code appended after the signature block
133+
# is, would be reported on stderr and otherwise ignored
134+
lines = ["$ErrorActionPreference = 'Stop'"] + list(steps) + [
135+
_dump_line(env.dumper, call='&')]
126136
return _run(
127137
['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File'],
128138
env.tmpdir.join('probe.ps1'), lines, 'powershell')

0 commit comments

Comments
 (0)