Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ on:
jobs:
test:
uses: ./.github/workflows/test.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
6 changes: 4 additions & 2 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
jobs:
test:
uses: ./.github/workflows/test.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

release-build:
needs: test
Expand All @@ -33,7 +35,7 @@ jobs:
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: python-packages
name: release-dists
path: dist/
retention-days: 5

Expand Down Expand Up @@ -69,4 +71,4 @@ jobs:
path: dist/

- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f
uses: pypa/gh-action-pypi-publish@release/v1
9 changes: 6 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: Test

on:
workflow_call:
secrets:
CODECOV_TOKEN:
required: true

jobs:
test:
Expand All @@ -20,9 +23,9 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v6
with:
node-version: '18'
node-version: '22'

- name: Install dependencies
run: |
Expand All @@ -48,7 +51,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests
name: codecov-umbrella

- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
Expand Down
11 changes: 8 additions & 3 deletions src/setuptools_nodejs/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,19 @@ def build_extension(

if not quiet:
logger.info(" ".join(install_command))

# Execute npm install
try:
stderr = subprocess.PIPE if quiet else None
# Use self.shell_enable from NodeJSCommand base class
# shell=True is needed on Windows for npm (.cmd files)
# shell=False on Unix-like systems to avoid argument parsing issues
check_subprocess_output(
install_command,
env=env,
stderr=stderr,
text=True,
encoding='utf-8',
shell=True,
shell=self.shell_enable,
cwd=source_dir,
)
except subprocess.CalledProcessError as e:
Expand All @@ -165,13 +167,16 @@ def build_extension(
# Execute npm run build
try:
stderr = subprocess.PIPE if quiet else None
# Use self.shell_enable from NodeJSCommand base class
# shell=True is needed on Windows for npm (.cmd files)
# shell=False on Unix-like systems to avoid argument parsing issues
check_subprocess_output(
build_command,
env=env,
stderr=stderr,
text=True,
encoding='utf-8',
shell=True,
shell=self.shell_enable,
cwd=source_dir,
)
except subprocess.CalledProcessError as e:
Expand Down
2 changes: 1 addition & 1 deletion src/setuptools_nodejs/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def run_for_extension(self, ext: NodeJSExtension) -> None:
env=ext.env,
text=True,
encoding='utf-8',
shell=True,
shell=self.shell_enable,
cwd=ext.source_dir
)
return # Successfully cleaned with npm
Expand Down
5 changes: 5 additions & 0 deletions src/setuptools_nodejs/command.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
import logging
import os
from setuptools import Command, Distribution
from typing import List, Optional

Expand All @@ -18,6 +19,10 @@ class NodeJSCommand(Command, ABC):

def initialize_options(self) -> None:
self.extensions: List[NodeJSExtension] = []
# Determine if shell should be enabled based on platform
# shell=True is needed on Windows for npm (.cmd files)
# shell=False on Unix-like systems to avoid argument parsing issues
self.shell_enable = os.name == "nt" # True on Windows, False on Unix-like systems

def finalize_options(self) -> None:
extensions: Optional[List[NodeJSExtension]] = getattr(
Expand Down
90 changes: 84 additions & 6 deletions tests/test_examples_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,27 @@ def get_local_package_path(package_name: str) -> str:
if line.startswith("Editable project location:"):
path = line.split(":", 1)[1].strip()
return path

# If not editable, try to find Location (for regular installs)
for line in result.stdout.splitlines():
if line.startswith("Location:"):
path = line.split(":", 1)[1].strip()
return path

raise RuntimeError(f"Could not find editable location for {package_name}")
# Fallback: use current directory if package is setuptools-nodejs
if package_name == "setuptools-nodejs":
# Try to find the package in the current project
project_root = Path(__file__).parent.parent
if (project_root / "src" / "setuptools_nodejs").exists():
return str(project_root)

raise RuntimeError(f"Could not find location for {package_name}")
except subprocess.CalledProcessError as e:
# Fallback for CI environment
if package_name == "setuptools-nodejs":
project_root = Path(__file__).parent.parent
if (project_root / "src" / "setuptools_nodejs").exists():
return str(project_root)
raise RuntimeError(f"Failed to get package info for {package_name}: {e}")


Expand Down Expand Up @@ -276,17 +294,77 @@ def test_example_project_build(project_dir: Path):
pyproject_file = tmp_project / "pyproject.toml"
modify_pyproject_with_local_path(pyproject_file, local_path)

# Run build command
# Run build command with npm cache directory to avoid permission issues
try:
subprocess.run(
# Create npm cache directory in temp dir
npm_cache_dir = tmpdir_path / '.npm_cache'
npm_cache_dir.mkdir(exist_ok=True)

# Set environment variables for npm
env = os.environ.copy()
env['npm_config_cache'] = str(npm_cache_dir)
# Use npm registry mirror for faster downloads in CI
env['npm_config_registry'] = 'https://registry.npmjs.org/'

# Check if npm is available
# On Windows, npm might be npm.cmd
npm_cmd = "npm.cmd" if os.name == "nt" else "npm"
npm_available = shutil.which(npm_cmd) is not None

if not npm_available:
# Try the other variant
npm_cmd = "npm" if os.name == "nt" else "npm.cmd"
npm_available = shutil.which(npm_cmd) is not None

if not npm_available:
pytest.skip("npm not available, skipping test")

# First, try to run npm install directly to see detailed errors
browser_dir = tmp_project / "browser"
if browser_dir.exists():
# Run npm install with detailed output
npm_result = subprocess.run(
[npm_cmd, "install"],
cwd=browser_dir,
capture_output=True,
text=True,
env=env
)
if npm_result.returncode != 0:
pytest.fail(
f"npm install failed for {project_dir.name}:\n"
f"npm STDOUT:\n{npm_result.stdout}\n"
f"npm STDERR:\n{npm_result.stderr}\n"
f"npm return code: {npm_result.returncode}"
)

# Then run the build
result = subprocess.run(
["python", "-m", "build", "--no-isolation"],
cwd=tmp_project,
check=True,
capture_output=True,
text=True
text=True,
env=env
)

if result.returncode != 0:
# Provide detailed error information
error_msg = (
f"Build failed for {project_dir.name} (return code: {result.returncode}):\n"
f"STDOUT:\n{result.stdout}\n"
f"STDERR:\n{result.stderr}\n"
f"Environment: npm_cache={npm_cache_dir}, registry={env['npm_config_registry']}\n"
)
pytest.fail(error_msg)

except subprocess.CalledProcessError as e:
pytest.fail(f"Build failed for {project_dir.name}:\n{e.stderr}")
# Fallback for check=True case
pytest.fail(
f"Build failed for {project_dir.name}:\n"
f"STDOUT:\n{e.stdout}\n"
f"STDERR:\n{e.stderr}\n"
f"Return code: {e.returncode}"
)

# Check dist directory exists
dist_dir = tmp_project / "dist"
Expand Down
Loading