ClickOnce를 통한 .NET Desktop Application + Embed Python 모듈 배포

안녕하세요. .NET Framework 4.8 WPF Application에서 python 스크립트를 호출하는 것을 고려하고 있습니다.

지금까지는 대상 PC에 Python3를 설치하고, 스크립트를 실행하기 위해서 필요한 python 모듈들을 선행하여 직접 설치 한 뒤, 프로그램에서 python 스크립트를 호출하고 있었습니다. (System.Diagnostics.Process를 통한 호출)

하지만 이제 조금 더 나아가서 대상 PC에 python을 사용자가 직접 설치하지 않고 고려하는 방법을 찾고 있습니다. 현재 찾아본 방법은,

  1. 대상 PC는 Windows 10 PC이므로 windows powershell 을 이용하여 python3를 설치하고 필요로하는 module을 연계하여 설치하는 방법.

예를 들어 이런 것입니다.

# Python이 설치되어 있는지 확인
$pythonInstalled = $null -ne (Get-Command python -ErrorAction SilentlyContinue)

if (-not $pythonInstalled) {
    Write-Output "Python이 설치되어 있지 않습니다. 설치를 시작합니다..."
    
    # 프로세서 아키텍처 확인
    $processorArchitecture = (Get-WmiObject -Class Win32_Processor).Architecture

    # Python 설치 프로그램 URL 설정
    switch ($processorArchitecture) {
        0 { # x86
            $installerUrl = "https://www.python.org/ftp/python/3.11.8/python-3.11.8.exe"
            break
        }
        9 { # x64 (AMD64)
            $installerUrl = "https://www.python.org/ftp/python/3.11.8/python-3.11.8-amd64.exe"
            break
        }
        12 { # ARM64
            $installerUrl = "https://www.python.org/ftp/python/3.11.8/python-3.11.8-arm64.exe"
            break
        }
        default {
            Write-Output "지원되지 않는 아키텍처입니다."
            exit
        }
    }

    # 다운로드할 위치와 파일 이름
    $installerPath = "$env:TEMP\python-installer.exe"
    
    # 설치 프로그램 다운로드
    Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath
    
    # Python 설치 (여기서는 기본 옵션으로 설치)
    Start-Process -FilePath $installerPath -Args "/quiet InstallAllUsers=1 PrependPath=1" -Wait
    
    # 설치 프로그램 삭제
    Remove-Item -Path $installerPath

    Write-Output "Python 설치가 완료되었습니다."
} else {
    Write-Output "Python이 이미 설치되어 있습니다."
}

# 필요한 Python 패키지 설치
$packages = @("six", "Pillow", "jinja2==3.0.3", "airtest", "markupsafe") # 필요한 패키지를 여기에 추가

foreach ($package in $packages) {
    $packageName = $package.Split('==')[0]
    $packageVersion = $package.Split('==')[1]
    $packageInfo = python -m pip show $packageName
    
    if (-not $packageInfo) {
        Write-Output "$packageName 설치를 시도합니다..."
        Start-Process -FilePath "python" -Args "-m pip install $package" -Wait
        Write-Output "$packageName 설치가 완료되었습니다."
    } else {
        Write-Output "$packageName 이미 설치되어 있습니다."
    }
}

Write-Output "모든 필요한 패키지 설치가 완료되었습니다."

위 파워쉘 코드를 실행하면 내 Windows PC에 특정 python3 버전이 설치되어 있는지 검사한 뒤 패키지를 설치합니다.

이후 .netfx wpf app을 통해 python 스크립트를 돌리는 것입니다.

  1. PyInstaller 이용.

python 스크립트는 스크립트 자체로 배포되는 만큼, 배포된 PC에서 스크립트 수정을 할 수 있기 때문에 스크립트 언어 자체가 가지는 보안적 취약점이 있으며, 마우스를 통해 쉽게 실행하지 못하고 터미널을 통해 실행해야 합니다.
그래서 이 PyInstaller 를 이용하면 python module 스크립트를 .exe 파일로 변형하여 내부에 python 설치기능까지 들어있는 채로 실행파일 형태로 배포할 수 있어서 1번의 방법보다 더 쉽게 설치가 가능합니다.

  1. Python.Included / Pythonnet 이용

이게 제가 궁금한 방법인데요.
.NET 에서 python 함께 폴리글랏하여 사용하는 방법에는 크게 IronPython과 pythonnet 이 있다고 할 수 있는데, IronPython은 Python을 주력언어로 사용하여 Python으로 Windows Forms를 한다던지 하는 용도로 사용할 수 있고, Python.NET은 python이 .NET 위에서 실행되어 .NET 어셈블리를 우리가 사용하듯 python 모듈을 .NET의 기능처럼 사용할 수 있게 하는 개념으로 이해를 하고 있습니다.
따라서 python .net의 가장 큰 장점은 C# 으로 새로 작성할 필요없이 이미 가지고 있는 python 스크립트를 python.net을 이용하여 그냥 실행하면 되는 것으로 이해하고 있습니다.
여기에 곁들여서 Python.Included 까지 이용하면 내가 배포하는 .NET Application에 Python을 내장하여 배포할 수 있는 것으로 이해했습니다.
즉 이 3번 방법을 이용하면 .NET 위에서 python이 실행되므로, 지원되는 pip 패키지와 버전만 맞다면 OS에 Python3를 설치하지 않아도 되는 것으로 이해했습니다.

따라서 1->2->3번의 순서대로 가장 Best한 방법이 될 것 같습니다.
1번은 이미 해놨지만, 3번이 가장 궁금합니다.
혹시 Python.Included를 이용해서 .NET Desktop Application을 배포해보신 분이 계실까요?

Q. 사용할 때 pip install을 통해 받을 수 있는 모듈과 내가 정의한 모듈 둘 다 사용할 수 있게 하는 코드는 어떤 형태일까요? 예제를 공유해주시면 감사드리겠습니다. 현재 저는 ClickOnce를 통해 배포하고 있습니다.

1 Like

도움안되는 방법 하나 알려드리자면
py파일을 그대로 소스파일 형태로 보내진 않을테니, 파이선 소스를 exe로 추출하여 같이 포함시켜서 배포하는겁니다.

1 Like

Python.NET은 CPython(python.exe)를 C#으로 Interop하는 라이브러리고

Python.Included는 파이썬을 자동으로 설치해주는 라이브러리입니다.
(패키지 버전과 동일한 파이썬을 설치합니다.)
즉, 최초 실행시 임시 폴더에 파이썬 설치와 패키지 설치하는 것으로 시작됩니다.

Add-Type -Path ".\Python.Included.dll" -ReferencedAssemblies ".\Python.Runtime.dll"
[Python.Included.Installer]::InstallDirectory = [System.IO.Path]::GetFullPath("Python")
[Python.Included.Installer]::SetupPython().Wait()

Python.Included를 사용하시고
최초 설치 시 위와 같이 Powershell 코드를 사용하여
파이썬과 PIP를 설치하시면 될 것 같습니다.

Installer.InstallDirectory = Path.GetFullPath("Python");
await Installer.SetupPython();

물론 C#에서도 동일하게 경로 지정해주셔야 되겠지요.

2 Likes

파이썬은 py 파일로 그대로 배포가 됩니다. 말씀하신 대로 하는 것이 2번에 소개한 PyInstaller를 사용하는 것입니다.

1 Like

파이썬을 설치한다고 보는 게 맞군요.
소스를 실행해봤는데 환경변수까지 설정하는 일반적인 설치는 아니라서 그런지 터미널에서 python -v 를 하면 동작하지 않습니다.
프로그램을 켤때마다 임시폴더에 설치가 되고 이후에 임시폴더를 참조해서 설치 없이 계속 쓰다가 임시폴더에서 지워지면 또 설치하고…그런식인가보군요.

제가 궁금했던 것은 pythonnet 을 이용하면 기존의 파이썬 파일을 수정없이 사용할 수 있는지 였습니다.

1 Like
  1. 명령 프롬프트를 통해 사용하시려면
    Python.Include를 사용하면, 별도 경로에 설치되므로. 해당 경로로 이동하여 Python.exe를 사용하면 됩니다.
  2. C#코드를 통해 파이썬 명령을 실행하시려면
    Py.Import를 통해서 py 파일을 실행하시면 됩니다.
// py 파일의 경로를 Path에 추가.
dynamic sys = Py.Import("sys");
sys.path.append(Environment.CurrentDirectory);

// py 파일 import 후 함수 실행
Py.Import("test").InvokeMethod("Run"); //InvokeMethod 또는 dynamic 사용

test.py를 실행하는 C# 코드

def Run():
    # ... 실행할 파이썬 코드 추가

test.py Python 코드

1 Like