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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Build script that ensures the CLI binary is executable before wheel packaging."""

import os
import stat
from pathlib import Path

from setuptools import setup
from setuptools.command.build_py import build_py


class BuildPyWithExecutableBinary(build_py):
"""Custom build_py that ensures the CLI binary has execute permissions."""

def run(self):
super().run()
# After files are copied to build dir, chmod the binary
if self.build_lib:
binary_path = Path(self.build_lib) / 'superdoc_sdk_cli_darwin_arm64' / 'bin' / 'superdoc'
if binary_path.exists():
current_mode = binary_path.stat().st_mode
binary_path.chmod(current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)


setup(cmdclass={'build_py': BuildPyWithExecutableBinary})
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@


def get_binary_path() -> str:
"""Return the absolute path to the bundled CLI binary, ensuring it is executable."""
"""Return the absolute path to the bundled CLI binary.

The binary is made executable at build time via setup.py's build_py hook,
so no runtime chmod is needed.
"""
binary = resources.files(__package__).joinpath('bin', _BINARY_NAME)
path = str(binary)

if not os.path.isfile(path):
raise FileNotFoundError(f'CLI binary not found: {path}')

if os.name != 'nt':
mode = os.stat(path).st_mode
os.chmod(path, mode | 0o111)

return path
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Build script that ensures the CLI binary is executable before wheel packaging."""

import os
import stat
from pathlib import Path

from setuptools import setup
from setuptools.command.build_py import build_py


class BuildPyWithExecutableBinary(build_py):
"""Custom build_py that ensures the CLI binary has execute permissions."""

def run(self):
super().run()
# After files are copied to build dir, chmod the binary
if self.build_lib:
binary_path = Path(self.build_lib) / 'superdoc_sdk_cli_darwin_x64' / 'bin' / 'superdoc'
if binary_path.exists():
current_mode = binary_path.stat().st_mode
binary_path.chmod(current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)


setup(cmdclass={'build_py': BuildPyWithExecutableBinary})
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@


def get_binary_path() -> str:
"""Return the absolute path to the bundled CLI binary, ensuring it is executable."""
"""Return the absolute path to the bundled CLI binary.

The binary is made executable at build time via setup.py's build_py hook,
so no runtime chmod is needed.
"""
binary = resources.files(__package__).joinpath('bin', _BINARY_NAME)
path = str(binary)

if not os.path.isfile(path):
raise FileNotFoundError(f'CLI binary not found: {path}')

if os.name != 'nt':
mode = os.stat(path).st_mode
os.chmod(path, mode | 0o111)

return path
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Build script that ensures the CLI binary is executable before wheel packaging."""

import os
import stat
from pathlib import Path

from setuptools import setup
from setuptools.command.build_py import build_py


class BuildPyWithExecutableBinary(build_py):
"""Custom build_py that ensures the CLI binary has execute permissions."""

def run(self):
super().run()
# After files are copied to build dir, chmod the binary
if self.build_lib:
binary_path = Path(self.build_lib) / 'superdoc_sdk_cli_linux_arm64' / 'bin' / 'superdoc'
if binary_path.exists():
current_mode = binary_path.stat().st_mode
binary_path.chmod(current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)


setup(cmdclass={'build_py': BuildPyWithExecutableBinary})
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@


def get_binary_path() -> str:
"""Return the absolute path to the bundled CLI binary, ensuring it is executable."""
"""Return the absolute path to the bundled CLI binary.

The binary is made executable at build time via setup.py's build_py hook,
so no runtime chmod is needed.
"""
binary = resources.files(__package__).joinpath('bin', _BINARY_NAME)
path = str(binary)

if not os.path.isfile(path):
raise FileNotFoundError(f'CLI binary not found: {path}')

if os.name != 'nt':
mode = os.stat(path).st_mode
os.chmod(path, mode | 0o111)

return path
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Build script that ensures the CLI binary is executable before wheel packaging."""

import os
import stat
from pathlib import Path

from setuptools import setup
from setuptools.command.build_py import build_py


class BuildPyWithExecutableBinary(build_py):
"""Custom build_py that ensures the CLI binary has execute permissions."""

def run(self):
super().run()
# After files are copied to build dir, chmod the binary
if self.build_lib:
binary_path = Path(self.build_lib) / 'superdoc_sdk_cli_linux_x64' / 'bin' / 'superdoc'
if binary_path.exists():
current_mode = binary_path.stat().st_mode
binary_path.chmod(current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)


setup(cmdclass={'build_py': BuildPyWithExecutableBinary})
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@


def get_binary_path() -> str:
"""Return the absolute path to the bundled CLI binary, ensuring it is executable."""
"""Return the absolute path to the bundled CLI binary.

The binary is made executable at build time via setup.py's build_py hook,
so no runtime chmod is needed.
"""
binary = resources.files(__package__).joinpath('bin', _BINARY_NAME)
path = str(binary)

if not os.path.isfile(path):
raise FileNotFoundError(f'CLI binary not found: {path}')

if os.name != 'nt':
mode = os.stat(path).st_mode
os.chmod(path, mode | 0o111)

return path
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Build script for Windows platform package.

Windows doesn't use Unix file permissions, so no chmod is needed.
This setup.py exists for consistency across all platform packages.
"""

from setuptools import setup

setup()
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@


def get_binary_path() -> str:
"""Return the absolute path to the bundled CLI binary, ensuring it is executable."""
"""Return the absolute path to the bundled CLI binary.

On Unix, the binary is made executable at build time via setup.py's build_py
hook. On Windows, executability is determined by file extension (.exe).
"""
binary = resources.files(__package__).joinpath('bin', _BINARY_NAME)
path = str(binary)

if not os.path.isfile(path):
raise FileNotFoundError(f'CLI binary not found: {path}')

if os.name != 'nt':
mode = os.stat(path).st_mode
os.chmod(path, mode | 0o111)

return path
14 changes: 5 additions & 9 deletions packages/sdk/langs/python/superdoc/embedded_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import os
import platform
from importlib import resources
from pathlib import Path
Expand Down Expand Up @@ -78,6 +77,11 @@ def _resolve_from_vendor_fallback(target: str) -> Optional[str]:


def resolve_embedded_cli_path() -> str:
"""Resolve the path to the embedded CLI binary for the current platform.

The binary is made executable at build time via each companion package's
setup.py build_py hook, so no runtime chmod is needed.
"""
target = _resolve_target()
if target is None:
raise SuperDocError(
Expand All @@ -102,12 +106,4 @@ def resolve_embedded_cli_path() -> str:
details={'target': target},
)

# Ensure binary is executable on unix
if os.name != 'nt':
try:
mode = os.stat(path).st_mode
os.chmod(path, mode | 0o111)
except Exception:
pass

return path
26 changes: 26 additions & 0 deletions packages/sdk/scripts/verify-python-companion-wheels.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ async function listWheelEntries(wheelPath) {
return JSON.parse(stdout);
}

async function getWheelEntryMode(wheelPath, entryName) {
// Zip files store Unix permissions in the high 16 bits of external_attr.
// external_attr >> 16 gives the Unix mode (e.g., 0o755 = 493 decimal).
const python = [
'import sys, zipfile',
'z = zipfile.ZipFile(sys.argv[1])',
'info = z.getinfo(sys.argv[2])',
'print(info.external_attr >> 16)',
].join('; ');
const { stdout } = await execFileAsync('python3', ['-c', python, wheelPath, entryName], {
cwd: REPO_ROOT,
env: process.env,
});
return parseInt(stdout.trim(), 10);
}

async function readWheelMetadata(wheelPath) {
const python = [
'import sys, zipfile',
Expand Down Expand Up @@ -202,6 +218,16 @@ async function verifySingleCompanion(wheelPath, target, errors) {
errors.push(`${target.id}: expected exactly 1 binary in bin/, found ${binEntries.length}: ${binEntries.join(', ')}`);
}

// Verify binary has execute permissions (Unix mode & 0o111 != 0)
// Skip for Windows — executability is determined by .exe extension, not file mode.
if (!target.id.startsWith('windows-') && binEntries.includes(expectedBinary)) {
const mode = await getWheelEntryMode(wheelPath, expectedBinary);
const hasExecuteBit = (mode & 0o111) !== 0;
if (!hasExecuteBit) {
errors.push(`${target.id}: binary missing execute permissions (mode=${mode.toString(8)})`);
}
}

console.log(` ${target.id} OK: ${path.basename(wheelPath)} (${(wheelStat.size / 1e6).toFixed(1)} MB)`);
}

Expand Down
Loading