@@ -13,6 +13,11 @@ import { validateBuildScripts } from '../config/security-validation';
1313import { TmpfsManager } from '../lib/tmpfs-manager' ;
1414import { githubDeploymentBaseDir , nsjailConfig } from '../config/nsjail' ;
1515import { selectBestPythonForDeployment } from '../utils/runtime-validator' ;
16+ import {
17+ isPyprojectSimpleScript ,
18+ parsePyprojectDependencies ,
19+ resolvePythonEntryPoint
20+ } from '../utils/python-helpers' ;
1621
1722/**
1823 * Parsed GitHub repository information
@@ -623,65 +628,6 @@ export class GitHubDeploymentHandler {
623628 }
624629 }
625630
626- /**
627- * Detect if pyproject.toml is a simple script (not installable package)
628- *
629- * A simple script is one that:
630- * 1. Has no package structure (no src/ dir, no matching package dir), OR
631- * 2. Has [build-system] but the build will fail due to missing structure
632- *
633- * We detect this by checking if common Python script files exist at root level
634- * (server.py, main.py, app.py, __main__.py) - indicating it's meant to run directly
635- */
636- async isPyprojectSimpleScript ( tempDir : string ) : Promise < boolean > {
637- const pyprojectPath = path . join ( tempDir , 'pyproject.toml' ) ;
638- try {
639- const content = await fs . promises . readFile ( pyprojectPath , 'utf8' ) ;
640-
641- // Check if pyproject.toml has [build-system] section
642- const hasBuildSystem = content . includes ( '[build-system]' ) ;
643-
644- // If no build-system, it's definitely a simple script (just dependency management)
645- if ( ! hasBuildSystem ) {
646- return true ;
647- }
648-
649- // Has build-system - check if package structure exists
650- // Look for src/ directory or package directory matching project name
651- const hasSrcDir = await fileExists ( path . join ( tempDir , 'src' ) ) ;
652-
653- // Extract package name from pyproject.toml
654- const nameMatch = content . match ( / ^ n a m e \s * = \s * " ( [ ^ " ] + ) " / m) ;
655- const packageName = nameMatch ? nameMatch [ 1 ] . replace ( / - / g, '_' ) : null ;
656- const hasPackageDir = packageName ? await fileExists ( path . join ( tempDir , packageName ) ) : false ;
657-
658- // If it has proper package structure (src/ or package dir), it's installable
659- if ( hasSrcDir || hasPackageDir ) {
660- return false ;
661- }
662-
663- // No package structure - check if there are standalone script files at root
664- const scriptFiles = [ 'server.py' , 'main.py' , 'app.py' , '__main__.py' ] ;
665- for ( const scriptFile of scriptFiles ) {
666- if ( await fileExists ( path . join ( tempDir , scriptFile ) ) ) {
667- // Found a standalone script - treat as simple script
668- this . logger . debug ( {
669- operation : 'python_pattern_detected' ,
670- pattern : 'simple_script' ,
671- reason : `Found ${ scriptFile } at root without package structure` ,
672- temp_dir : tempDir
673- } , `Detected simple Python script pattern (${ scriptFile } without package structure)` ) ;
674- return true ;
675- }
676- }
677-
678- // Has build-system, has scripts, but no package structure and no standalone scripts
679- // This will likely fail to build - treat as simple script to avoid build errors
680- return true ;
681- } catch {
682- return false ;
683- }
684- }
685631
686632 /**
687633 * Install Python dependencies using uv or pip
@@ -721,8 +667,8 @@ export class GitHubDeploymentHandler {
721667 let args : string [ ] ;
722668 let installMethod : string ;
723669
724- // Determine installation method
725- if ( hasPyproject && ! ( await this . isPyprojectSimpleScript ( tempDir ) ) ) {
670+ // Determine installation method using pattern detection from python-helpers
671+ if ( hasPyproject && ! ( await isPyprojectSimpleScript ( tempDir ) ) ) {
726672 // Pattern 1: Installable package with pyproject.toml
727673 // Use uv sync to create venv and install package as editable
728674 command = 'uv' ;
@@ -867,39 +813,19 @@ export class GitHubDeploymentHandler {
867813 let pipArgs : string [ ] ;
868814
869815 if ( hasPyproject ) {
870- // Parse dependencies from pyproject.toml and install them directly
816+ // Parse dependencies from pyproject.toml using extracted helper
871817 const pyprojectPath = path . join ( tempDir , 'pyproject.toml' ) ;
872- const pyprojectContent = await fs . promises . readFile ( pyprojectPath , 'utf8' ) ;
873-
874- // Extract dependencies array from [project] section
875- const depsMatch = pyprojectContent . match ( / d e p e n d e n c i e s \s * = \s * \[ ( [ \s \S ] * ?) \] / ) ;
876- if ( depsMatch ) {
877- const depsContent = depsMatch [ 1 ] ;
878- // Parse individual dependencies (handle both "pkg" and 'pkg' quotes)
879- const deps = depsContent
880- . split ( ',' )
881- . map ( d => d . trim ( ) )
882- . filter ( d => d . length > 0 )
883- . map ( d => d . replace ( / ^ [ " ' ] | [ " ' ] $ / g, '' ) ) ; // Remove quotes
818+ const deps = await parsePyprojectDependencies ( pyprojectPath ) ;
884819
885- if ( deps . length > 0 ) {
886- pipArgs = [ 'pip' , 'install' , ...deps ] ;
887- } else {
888- // No dependencies found, skip install
889- this . logger . info ( {
890- operation : 'python_deps_skip' ,
891- temp_dir : tempDir ,
892- reason : 'no_dependencies_in_pyproject'
893- } , 'No dependencies found in pyproject.toml, skipping install' ) ;
894- pipArgs = [ ] ;
895- }
820+ if ( deps . length > 0 ) {
821+ pipArgs = [ 'pip' , 'install' , ...deps ] ;
896822 } else {
897- // No dependencies section found
823+ // No dependencies found, skip install
898824 this . logger . info ( {
899825 operation : 'python_deps_skip' ,
900826 temp_dir : tempDir ,
901- reason : 'no_dependencies_section '
902- } , 'No [project. dependencies] section in pyproject.toml, skipping install' ) ;
827+ reason : 'no_dependencies_in_pyproject '
828+ } , 'No dependencies found in pyproject.toml, skipping install' ) ;
903829 pipArgs = [ ] ;
904830 }
905831 } else {
@@ -1104,111 +1030,28 @@ export class GitHubDeploymentHandler {
11041030
11051031 /**
11061032 * Resolve Python package entry point from pyproject.toml or __main__.py
1033+ * Delegates to resolvePythonEntryPoint() from python-helpers.ts
11071034 */
11081035 async resolvePythonPackageEntry ( tempDir : string ) : Promise < { command : string ; entryPoint : string } > {
11091036 this . logger . debug ( {
11101037 operation : 'python_entry_resolve_start' ,
11111038 temp_dir : tempDir
11121039 } , 'Resolving Python package entry point' ) ;
11131040
1114- // Try pyproject.toml first
1115- const pyprojectPath = path . join ( tempDir , 'pyproject.toml' ) ;
1116- if ( await fileExists ( pyprojectPath ) ) {
1117- const content = await fs . promises . readFile ( pyprojectPath , 'utf8' ) ;
1118-
1119- // Look for [project.scripts] section
1120- const scriptsMatch = content . match ( / \[ p r o j e c t \. s c r i p t s \] \s * \n ( [ ^ [ ] + ) / ) ;
1121- if ( scriptsMatch ) {
1122- const firstScriptMatch = scriptsMatch [ 1 ] . match ( / ^ ( \w + ) \s * = / m) ;
1123- if ( firstScriptMatch ) {
1124- const scriptName = firstScriptMatch [ 1 ] ;
1125- // Entry point is installed in .venv/bin/ after uv sync
1126- const entryPoint = path . join ( tempDir , '.venv' , 'bin' , scriptName ) ;
1127-
1128- this . logger . info ( {
1129- operation : 'python_entry_resolved' ,
1130- temp_dir : tempDir ,
1131- script_name : scriptName ,
1132- entry_point : entryPoint
1133- } , `Resolved Python entry point: ${ scriptName } ` ) ;
1134-
1135- return { command : entryPoint , entryPoint } ;
1136- }
1137- }
1138-
1139- // Look for [project.gui-scripts] as fallback
1140- const guiMatch = content . match ( / \[ p r o j e c t \. g u i - s c r i p t s \] \s * \n ( [ ^ [ ] + ) / ) ;
1141- if ( guiMatch ) {
1142- const firstScriptMatch = guiMatch [ 1 ] . match ( / ^ ( \w + ) \s * = / m) ;
1143- if ( firstScriptMatch ) {
1144- const scriptName = firstScriptMatch [ 1 ] ;
1145- const entryPoint = path . join ( tempDir , '.venv' , 'bin' , scriptName ) ;
1041+ const result = await resolvePythonEntryPoint ( tempDir ) ;
11461042
1147- this . logger . info ( {
1148- operation : 'python_entry_resolved' ,
1149- temp_dir : tempDir ,
1150- script_name : scriptName ,
1151- entry_point : entryPoint
1152- } , `Resolved Python GUI entry point: ${ scriptName } ` ) ;
1153-
1154- return { command : entryPoint , entryPoint } ;
1155- }
1156- }
1043+ if ( ! result ) {
1044+ throw new Error ( 'Cannot resolve Python entry point: no scripts in pyproject.toml, no __main__.py, and no common script files (server.py, main.py, app.py, run.py) found' ) ;
11571045 }
11581046
1159- // Fallback: look for __main__.py
1160- const mainPath = path . join ( tempDir , '__main__.py' ) ;
1161- if ( await fileExists ( mainPath ) ) {
1162- this . logger . info ( {
1163- operation : 'python_entry_resolved_main' ,
1164- temp_dir : tempDir ,
1165- entry_point : mainPath
1166- } , 'Using __main__.py as entry point' ) ;
1167-
1168- // Use venv Python if available
1169- const venvPython = path . join ( tempDir , '.venv' , 'bin' , 'python' ) ;
1170- const command = await fileExists ( venvPython ) ? venvPython : 'python3' ;
1171-
1172- return { command, entryPoint : mainPath } ;
1173- }
1174-
1175- // Try src/__main__.py (common pattern)
1176- const srcMainPath = path . join ( tempDir , 'src' , '__main__.py' ) ;
1177- if ( await fileExists ( srcMainPath ) ) {
1178- this . logger . info ( {
1179- operation : 'python_entry_resolved_src_main' ,
1180- temp_dir : tempDir ,
1181- entry_point : srcMainPath
1182- } , 'Using src/__main__.py as entry point' ) ;
1183-
1184- // Use venv Python if available
1185- const venvPython = path . join ( tempDir , '.venv' , 'bin' , 'python' ) ;
1186- const command = await fileExists ( venvPython ) ? venvPython : 'python3' ;
1187-
1188- return { command, entryPoint : srcMainPath } ;
1189- }
1190-
1191- // Try common script names (server.py, main.py, app.py)
1192- const commonScriptNames = [ 'server.py' , 'main.py' , 'app.py' , 'run.py' ] ;
1193- for ( const scriptName of commonScriptNames ) {
1194- const scriptPath = path . join ( tempDir , scriptName ) ;
1195- if ( await fileExists ( scriptPath ) ) {
1196- this . logger . info ( {
1197- operation : 'python_entry_resolved_script' ,
1198- temp_dir : tempDir ,
1199- script_name : scriptName ,
1200- entry_point : scriptPath
1201- } , `Using ${ scriptName } as entry point` ) ;
1202-
1203- // Use venv Python if available
1204- const venvPython = path . join ( tempDir , '.venv' , 'bin' , 'python' ) ;
1205- const command = await fileExists ( venvPython ) ? venvPython : 'python3' ;
1206-
1207- return { command, entryPoint : scriptPath } ;
1208- }
1209- }
1047+ this . logger . info ( {
1048+ operation : 'python_entry_resolved' ,
1049+ temp_dir : tempDir ,
1050+ command : result . command ,
1051+ entry_point : result . entryPoint
1052+ } , `Resolved Python entry point: ${ result . command } ` ) ;
12101053
1211- throw new Error ( 'Cannot resolve Python entry point: no scripts in pyproject.toml, no __main__.py, and no common script files (server.py, main.py, app.py) found' ) ;
1054+ return result ;
12121055 }
12131056
12141057 /**
0 commit comments