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
141 changes: 101 additions & 40 deletions src/Microsoft.Agents.A365.DevTools.Cli/Services/PythonBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@ public async Task<bool> ValidateEnvironmentAsync()
{
_logger.LogInformation("Validating Python environment...");

_pythonExe = await PythonLocator.FindPythonExecutableAsync(_executor);
if (string.IsNullOrWhiteSpace(_pythonExe))
{
_pythonExe = await PythonLocator.FindPythonExecutableAsync(_executor);
if (string.IsNullOrWhiteSpace(_pythonExe))
{
_logger.LogError("Python not found. Please install Python from https://www.python.org/");
throw new PythonLocatorException("Python executable could not be located.");
}
}

var pythonResult = await _executor.ExecuteAsync(_pythonExe, "--version", captureOutput: true);
if (!pythonResult.Success)
{
_logger.LogError("Python not found. Please install Python from https://www.python.org/");
throw new PythonLocatorException("Python executable could not be located.");
}
}

var pipResult = await _executor.ExecuteAsync(_pythonExe, "-m pip --version", captureOutput: true);
if (!pipResult.Success)
{
Expand Down Expand Up @@ -139,28 +139,28 @@ public async Task CleanAsync(string projectDir)

public async Task<string> BuildAsync(string projectDir, string outputPath, bool verbose)
{
// Clean up old publish directory for fresh start
var publishPath = Path.Combine(projectDir, outputPath);
// Clean up old publish directory for fresh start
var publishPath = Path.Combine(projectDir, outputPath);

if (Directory.Exists(publishPath))
{
_logger.LogInformation("Removing old publish directory...");
Directory.Delete(publishPath, recursive: true);
}
_logger.LogInformation("Building Python project...");
// Run python -m py_compile on all .py files at the project root to catch syntax errors before packaging
var pyFiles = Directory.GetFiles(projectDir, "*.py", SearchOption.TopDirectoryOnly);
foreach (var pyFile in pyFiles)
{
var result = await _executor.ExecuteAsync(_pythonExe!, $"-m py_compile \"{pyFile}\"", projectDir, captureOutput: true);
if (!result.Success)
{
_logger.LogError("Python syntax error in {File}:\n{Error}", pyFile, result.StandardError);
throw new DeployAppPythonCompileException($"Python syntax error in {pyFile}:\n{result.StandardError}");
}
}

_logger.LogInformation("Building Python project...");
// Run python -m py_compile on all .py files at the project root to catch syntax errors before packaging
var pyFiles = Directory.GetFiles(projectDir, "*.py", SearchOption.TopDirectoryOnly);
foreach (var pyFile in pyFiles)
{
var result = await _executor.ExecuteAsync(_pythonExe!, $"-m py_compile \"{pyFile}\"", projectDir, captureOutput: true);
if (!result.Success)
{
_logger.LogError("Python syntax error in {File}:\n{Error}", pyFile, result.StandardError);
throw new DeployAppPythonCompileException($"Python syntax error in {pyFile}:\n{result.StandardError}");
}
}

Directory.CreateDirectory(publishPath);

// Step 1: Copy entire project structure (excluding unwanted files)
Expand All @@ -186,7 +186,8 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
await EnsureLocalPackagesExistInPublish(publishPath, publishDist, verbose);

// Step 4: Create requirements.txt for Azure deployment
await CreateAzureRequirementsTxt(publishPath, verbose);
// Pass projectDir to detect whether to use editable install or copy existing requirements.txt
await CreateAzureRequirementsTxt(projectDir, publishPath, verbose);

// Step 4.5: Create .deployment file to force Oryx build
await CreateDeploymentFile(publishPath);
Expand Down Expand Up @@ -224,13 +225,13 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
}
}
else
{
// Try to get from current python
_pythonExe = _pythonExe ?? await PythonLocator.FindPythonExecutableAsync(_executor);
if (string.IsNullOrWhiteSpace(_pythonExe))
{
_logger.LogError("Python not found. Please install Python from https://www.python.org/");
throw new PythonLocatorException("Python executable could not be located.");
{
// Try to get from current python
_pythonExe = _pythonExe ?? await PythonLocator.FindPythonExecutableAsync(_executor);
if (string.IsNullOrWhiteSpace(_pythonExe))
{
_logger.LogError("Python not found. Please install Python from https://www.python.org/");
throw new PythonLocatorException("Python executable could not be located.");
}
var versionResult = await _executor.ExecuteAsync(_pythonExe, "--version", captureOutput: true);
if (versionResult.Success)
Expand Down Expand Up @@ -545,16 +546,76 @@ private async Task EnsureLocalPackagesExistInPublish(string publishPath, string
await Task.CompletedTask;
}

private async Task CreateAzureRequirementsTxt(string publishPath, bool verbose)
/// <summary>
/// Creates requirements.txt for Azure deployment.
/// For projects with pyproject.toml or setup.py, uses editable install (-e .).
/// For projects with only requirements.txt, copies the existing file to preserve dependencies.
/// </summary>
private async Task CreateAzureRequirementsTxt(string projectDir, string publishPath, bool verbose)
{
var requirementsTxt = Path.Combine(publishPath, "requirements.txt");

// Azure-native requirements.txt that mirrors local workflow
// --pre allows installation of pre-release versions
var content = "--find-links dist\n--pre\n-e .\n";

await File.WriteAllTextAsync(requirementsTxt, content);
_logger.LogInformation("Created requirements.txt for Azure deployment");
var hasPyProject = File.Exists(Path.Combine(projectDir, "pyproject.toml"));
var hasSetupPy = File.Exists(Path.Combine(projectDir, "setup.py"));
var sourceRequirements = Path.Combine(projectDir, "requirements.txt");

if (verbose)
{
_logger.LogDebug("Checking project structure: pyproject.toml={HasPyProject}, setup.py={HasSetupPy}, requirements.txt={HasRequirements}",
hasPyProject, hasSetupPy, File.Exists(sourceRequirements));
}

if (hasPyProject || hasSetupPy)
{
// Check if requirements.txt also exists and log a warning
if (File.Exists(sourceRequirements))
{
_logger.LogWarning(
"Both pyproject.toml/setup.py and requirements.txt exist. " +
"Using editable install approach (pyproject.toml takes precedence). " +
"Dependencies from requirements.txt will be ignored - ensure they're declared in pyproject.toml instead.");
}

// Use editable install approach for projects with pyproject.toml/setup.py
// This allows pip to install the project as a package
_logger.LogInformation("Detected pyproject.toml or setup.py - using editable install approach");
var content = "--find-links dist\n--pre\n-e .\n";
await File.WriteAllTextAsync(requirementsTxt, content);
_logger.LogInformation("Created requirements.txt for editable install");
}
else if (File.Exists(sourceRequirements))
{
// Copy existing requirements.txt for projects without pyproject.toml/setup.py
// This preserves the original dependency list
_logger.LogInformation("No pyproject.toml or setup.py found - copying existing requirements.txt");

try
{
File.Copy(sourceRequirements, requirementsTxt, overwrite: true);
_logger.LogInformation("Copied existing requirements.txt to publish folder");
}
catch (FileNotFoundException ex)
{
_logger.LogError(ex, "Source requirements.txt not found: {Path}", sourceRequirements);
throw new DeployAppException($"Cannot find requirements.txt at {sourceRequirements}. The file may have been deleted.", ex);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Access denied when copying requirements.txt");
throw new DeployAppException($"Permission denied: Cannot copy requirements.txt to {requirementsTxt}. Check file permissions.", ex);
}
catch (IOException ex)
{
_logger.LogError(ex, "Failed to copy requirements.txt");
throw new DeployAppException($"Failed to copy requirements.txt to publish folder. The file may be in use or disk may be full.", ex);
}
}
else
{
// No requirements file found - create a minimal one
_logger.LogWarning("No requirements.txt or pyproject.toml found - creating minimal requirements.txt");
var content = "# Auto-generated - add your dependencies here\n";
await File.WriteAllTextAsync(requirementsTxt, content);
}
}

private async Task CreateDeploymentFile(string publishPath)
Expand Down
Loading
Loading