diff --git a/.gitignore b/.gitignore index 9532800..6670a78 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ # # Resource files are binaries containing manifest, project icon and version info. # They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files. -#*.res +*.res # # Type library file (binary). In old Delphi versions it should be stored. # Since Delphi 2009 it is produced from .ridl file and can safely be ignored. diff --git a/packages/P4DEnvironment.dpk b/packages/P4DEnvironment.dpk index 5cf1f0f..88d2c84 100644 --- a/packages/P4DEnvironment.dpk +++ b/packages/P4DEnvironment.dpk @@ -9,21 +9,21 @@ package P4DEnvironment; {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} -{$LOCALSYMBOLS ON} +{$LOCALSYMBOLS OFF} {$LONGSTRINGS ON} {$OPENSTRINGS ON} -{$OPTIMIZATION OFF} -{$OVERFLOWCHECKS ON} -{$RANGECHECKS ON} -{$REFERENCEINFO ON} +{$OPTIMIZATION ON} +{$OVERFLOWCHECKS OFF} +{$RANGECHECKS OFF} +{$REFERENCEINFO OFF} {$SAFEDIVIDE OFF} -{$STACKFRAMES ON} +{$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST OFF} {$MINENUMSIZE 1} {$IMAGEBASE $400000} -{$DEFINE DEBUG} +{$DEFINE RELEASE} {$ENDIF IMPLICITBUILDING} {$LIBSUFFIX AUTO} {$RUNONLY} @@ -32,7 +32,8 @@ package P4DEnvironment; requires rtl, python, - p4dtools; + p4dtools, + p4denvironmentproject; contains PyEnvironment in '..\src\PyEnvironment.pas', diff --git a/packages/P4DEnvironment.dproj b/packages/P4DEnvironment.dproj index 31f5e0d..080227d 100644 --- a/packages/P4DEnvironment.dproj +++ b/packages/P4DEnvironment.dproj @@ -5,7 +5,7 @@ Release None P4DEnvironment.dpk - Win64 + Win32 {08C84E96-1508-43E8-87F7-A4B2F8AF2205} 19.4 168083 @@ -71,6 +71,12 @@ Base true + + true + Cfg_2 + true + true + P4DEnvironment ..\resources @@ -138,6 +144,8 @@ false + C:\Program Files (x86)\Embarcadero\Studio\22.0\bin\bds.exe + C:\Users\lucas\OneDrive\Documents\Embarcadero\Studio\Projects\deployabletest\PyDeploy.dpr true 1033 @@ -147,6 +155,12 @@ false 0 + + C:\Program Files (x86)\Embarcadero\Studio\22.0\bin\bds.exe + C:\Users\lucas\OneDrive\Documents\Embarcadero\Studio\Projects\deployabletest\PyDeploy.dpr + true + 1033 + MainSource @@ -154,6 +168,7 @@ + diff --git a/packages/P4DEnvironmentProject.dpk b/packages/P4DEnvironmentProject.dpk new file mode 100644 index 0000000..01c9fff --- /dev/null +++ b/packages/P4DEnvironmentProject.dpk @@ -0,0 +1,38 @@ +package P4DEnvironmentProject; + +{$R *.res} +{$IFDEF IMPLICITBUILDING This IFDEF should not be used by users} +{$ALIGN 8} +{$ASSERTIONS ON} +{$BOOLEVAL OFF} +{$DEBUGINFO OFF} +{$EXTENDEDSYNTAX ON} +{$IMPORTEDDATA ON} +{$IOCHECKS ON} +{$LOCALSYMBOLS ON} +{$LONGSTRINGS ON} +{$OPENSTRINGS ON} +{$OPTIMIZATION OFF} +{$OVERFLOWCHECKS ON} +{$RANGECHECKS ON} +{$REFERENCEINFO ON} +{$SAFEDIVIDE OFF} +{$STACKFRAMES ON} +{$TYPEDADDRESS OFF} +{$VARSTRINGCHECKS ON} +{$WRITEABLECONST OFF} +{$MINENUMSIZE 1} +{$IMAGEBASE $400000} +{$DEFINE DEBUG} +{$ENDIF IMPLICITBUILDING} +{$LIBSUFFIX AUTO} +{$RUNONLY} +{$IMPLICITBUILD ON} + +requires + rtl; + +contains + PyEnvironment.Project in '..\src\Project\PyEnvironment.Project.pas'; + +end. diff --git a/packages/P4DEnvironmentProject.dproj b/packages/P4DEnvironmentProject.dproj new file mode 100644 index 0000000..9bfee5a --- /dev/null +++ b/packages/P4DEnvironmentProject.dproj @@ -0,0 +1,168 @@ + + + True + Package + Release + None + P4DEnvironmentProject.dpk + Win32 + {D7D611D7-E03F-4578-8243-2338F9455A17} + 19.4 + 168083 + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + P4DEnvironmentProject + All + ..\lib\$(Platform)\$(Config) + .\$(Platform)\$(Config) + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + $(Auto) + true + true + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + Debug + package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.0.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey= + + + Debug + package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.0.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey= + + + Debug + CFBundleName=$(MSBuildProjectName);CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleVersion=1.0.0;CFBundleShortVersionString=1.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);NSHighResolutionCapable=true;LSApplicationCategoryType=public.app-category.utilities;NSLocationUsageDescription=The reason for accessing the location information of the user;NSContactsUsageDescription=The reason for accessing the contacts;NSCalendarsUsageDescription=The reason for accessing the calendar data;NSRemindersUsageDescription=The reason for accessing the reminders;NSCameraUsageDescription=The reason for accessing the camera;NSMicrophoneUsageDescription=The reason for accessing the microphone;NSMotionUsageDescription=The reason for accessing the accelerometer;NSDesktopFolderUsageDescription=The reason for accessing the Desktop folder;NSDocumentsFolderUsageDescription=The reason for accessing the Documents folder;NSDownloadsFolderUsageDescription=The reason for accessing the Downloads folder;NSNetworkVolumesUsageDescription=The reason for accessing files on a network volume;NSRemovableVolumesUsageDescription=The reason for accessing files on a removable volume;NSSpeechRecognitionUsageDescription=The reason for requesting to send user data to Apple's speech recognition servers + + + Debug + CFBundleName=$(MSBuildProjectName);CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleVersion=1.0.0;CFBundleShortVersionString=1.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);NSHighResolutionCapable=true;LSApplicationCategoryType=public.app-category.utilities;NSLocationUsageDescription=The reason for accessing the location information of the user;NSContactsUsageDescription=The reason for accessing the contacts;NSCalendarsUsageDescription=The reason for accessing the calendar data;NSRemindersUsageDescription=The reason for accessing the reminders;NSCameraUsageDescription=The reason for accessing the camera;NSMicrophoneUsageDescription=The reason for accessing the microphone;NSMotionUsageDescription=The reason for accessing the accelerometer;NSDesktopFolderUsageDescription=The reason for accessing the Desktop folder;NSDocumentsFolderUsageDescription=The reason for accessing the Documents folder;NSDownloadsFolderUsageDescription=The reason for accessing the Downloads folder;NSNetworkVolumesUsageDescription=The reason for accessing files on a network volume;NSRemovableVolumesUsageDescription=The reason for accessing files on a removable volume;NSSpeechRecognitionUsageDescription=The reason for requesting to send user data to Apple's speech recognition servers + + + Debug + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + Debug + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + true + + + true + true + DEBUG;$(DCC_Define) + true + true + false + true + true + + + false + true + 1033 + + + 0 + RELEASE;$(DCC_Define) + false + 0 + + + + MainSource + + + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + Delphi.Personality.12 + Package + + + + P4DEnvironmentProject.dpk + + + + + True + True + True + True + True + True + True + False + + + 12 + + + + + diff --git a/packages/P4DPythonEnvironmentsComponentSuite.groupproj b/packages/P4DPythonEnvironmentsComponentSuite.groupproj index 1170405..60160df 100644 --- a/packages/P4DPythonEnvironmentsComponentSuite.groupproj +++ b/packages/P4DPythonEnvironmentsComponentSuite.groupproj @@ -6,11 +6,17 @@ - + + + P4DEnvironmentProject.dproj + + + P4DTools.dproj;P4DEnvironmentProject.dproj + - + P4DEnvironment.dproj @@ -29,32 +35,50 @@ - + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + diff --git a/packages/P4DTools.dproj b/packages/P4DTools.dproj index e6445b6..ea58d03 100644 --- a/packages/P4DTools.dproj +++ b/packages/P4DTools.dproj @@ -5,7 +5,7 @@ Release None P4DTools.dpk - Win64 + Win32 {ACD248FC-DD28-4E84-9959-B3E7F13EF694} 19.4 168083 diff --git a/packages/dclP4DEnvironment.dpk b/packages/dclP4DEnvironment.dpk index db6a81f..42c8f9b 100644 --- a/packages/dclP4DEnvironment.dpk +++ b/packages/dclP4DEnvironment.dpk @@ -10,21 +10,21 @@ package dclP4DEnvironment; {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} -{$LOCALSYMBOLS ON} +{$LOCALSYMBOLS OFF} {$LONGSTRINGS ON} {$OPENSTRINGS ON} -{$OPTIMIZATION OFF} -{$OVERFLOWCHECKS ON} -{$RANGECHECKS ON} -{$REFERENCEINFO ON} +{$OPTIMIZATION ON} +{$OVERFLOWCHECKS OFF} +{$RANGECHECKS OFF} +{$REFERENCEINFO OFF} {$SAFEDIVIDE OFF} -{$STACKFRAMES ON} +{$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST OFF} {$MINENUMSIZE 1} {$IMAGEBASE $400000} -{$DEFINE DEBUG} +{$DEFINE RELEASE} {$ENDIF IMPLICITBUILDING} {$DESCRIPTION 'P4D AI&ML - Python environments'} {$LIBSUFFIX AUTO} @@ -33,9 +33,12 @@ package dclP4DEnvironment; requires rtl, - p4denvironment; + p4denvironment, + designide, + dclp4denvironmentproject; contains - PyEnvironment.Registration in '..\src\PyEnvironment.Registration.pas'; + PyEnvironment.Registration in '..\src\PyEnvironment.Registration.pas', + PyEnvionment.Editors in '..\src\PyEnvionment.Editors.pas'; end. diff --git a/packages/dclP4DEnvironment.dproj b/packages/dclP4DEnvironment.dproj index 7d36329..46e1dc9 100644 --- a/packages/dclP4DEnvironment.dproj +++ b/packages/dclP4DEnvironment.dproj @@ -94,7 +94,10 @@ + + + RCDATA TPYEMBEDDEDENVIRONMENT128_PNG diff --git a/packages/dclP4DEnvironmentProject.dpk b/packages/dclP4DEnvironmentProject.dpk new file mode 100644 index 0000000..42857b1 --- /dev/null +++ b/packages/dclP4DEnvironmentProject.dpk @@ -0,0 +1,47 @@ +package dclP4DEnvironmentProject; + +{$R *.res} +{$IFDEF IMPLICITBUILDING This IFDEF should not be used by users} +{$ALIGN 8} +{$ASSERTIONS ON} +{$BOOLEVAL OFF} +{$DEBUGINFO OFF} +{$EXTENDEDSYNTAX ON} +{$IMPORTEDDATA ON} +{$IOCHECKS ON} +{$LOCALSYMBOLS ON} +{$LONGSTRINGS ON} +{$OPENSTRINGS ON} +{$OPTIMIZATION OFF} +{$OVERFLOWCHECKS ON} +{$RANGECHECKS ON} +{$REFERENCEINFO ON} +{$SAFEDIVIDE OFF} +{$STACKFRAMES ON} +{$TYPEDADDRESS OFF} +{$VARSTRINGCHECKS ON} +{$WRITEABLECONST OFF} +{$MINENUMSIZE 1} +{$IMAGEBASE $400000} +{$DEFINE DEBUG} +{$ENDIF IMPLICITBUILDING} +{$DESCRIPTION 'P4D Environments IDE Extension'} +{$LIBSUFFIX AUTO} +{$DESIGNONLY} +{$IMPLICITBUILD ON} + +requires + rtl, + designide, + P4DEnvironmentProject; + +contains + PyEnvironment.Project.IDE.Helper in '..\src\Project\IDE\PyEnvironment.Project.IDE.Helper.pas', + PyEnvironment.Project.IDE.ManagerMenu in '..\src\Project\IDE\PyEnvironment.Project.IDE.ManagerMenu.pas', + PyEnvironment.Project.IDE.Menu in '..\src\Project\IDE\PyEnvironment.Project.IDE.Menu.pas', + PyEnvironment.Project.IDE.Deploy in '..\src\Project\IDE\PyEnvironment.Project.IDE.Deploy.pas', + PyEnvironment.Project.IDE.Registration in '..\src\Project\IDE\PyEnvironment.Project.IDE.Registration.pas', + PyEnvironment.Project.IDE.Types in '..\src\Project\IDE\PyEnvironment.Project.IDE.Types.pas'; + +end. + diff --git a/packages/dclP4DEnvironmentProject.dproj b/packages/dclP4DEnvironmentProject.dproj new file mode 100644 index 0000000..a84de81 --- /dev/null +++ b/packages/dclP4DEnvironmentProject.dproj @@ -0,0 +1,132 @@ + + + True + Package + Release + None + dclP4DEnvironmentProject.dpk + Win32 + {4FE390C6-126B-4D5E-B1DF-8624DAB3A6CE} + 19.4 + 1 + + + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + dclP4DEnvironmentProject + All + ..\lib\$(Platform)\$(Config) + P4D Environments IDE Extension + .\$(Platform)\$(Config) + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + true + $(Auto) + true + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + Debug + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + rtl;$(DCC_UsePackage) + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + true + true + DEBUG;$(DCC_Define) + true + true + false + true + true + + + false + C:\Program Files (x86)\Embarcadero\Studio\22.0\bin\bds.exe + C:\Users\lucas\OneDrive\Documents\Embarcadero\Studio\Projects\deployabletest\Project1.dpr + true + + + 0 + RELEASE;$(DCC_Define) + false + 0 + + + + MainSource + + + + + + + + + + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + Delphi.Personality.12 + Package + + + + dclP4DEnvironmentProject.dpk + + + + + False + False + False + False + False + True + False + False + + + 12 + + + + + diff --git a/src/AddOn/PyEnvironment.AddOn.EnsurePip.pas b/src/AddOn/PyEnvironment.AddOn.EnsurePip.pas index 60c8d10..de06d37 100644 --- a/src/AddOn/PyEnvironment.AddOn.EnsurePip.pas +++ b/src/AddOn/PyEnvironment.AddOn.EnsurePip.pas @@ -77,6 +77,7 @@ procedure TPyEnvironmentAddOnEnsurePip.InternalExecute( LOutput: string; begin inherited; + { TODO : Check for valida executable and shared library files } if (TExecCmdService.Cmd(ADistribution.Executable, TExecCmdArgs.BuildArgv( ADistribution.Executable, ['-m', 'pip', '--version']), diff --git a/src/Embeddable/PyEnvironment.Embeddable.pas b/src/Embeddable/PyEnvironment.Embeddable.pas index 1219fd0..bcc068e 100644 --- a/src/Embeddable/PyEnvironment.Embeddable.pas +++ b/src/Embeddable/PyEnvironment.Embeddable.pas @@ -89,12 +89,19 @@ TPyCustomEmbeddableDistribution = class(TPyDistribution) TPyEmbeddableDistribution = class(TPyCustomEmbeddableDistribution) private FScanned: boolean; + FDeleteEmbeddable: boolean; + procedure DoDeleteEmbeddable(); protected procedure LoadSettings(); override; public + procedure Setup(); override; property Scanned: boolean read FScanned write FScanned; published property EmbeddablePackage; + /// + /// Delete the embeddable zip file after install. + /// + property DeleteEmbeddable: boolean read FDeleteEmbeddable write FDeleteEmbeddable; end; TPyEmbeddableCustomCollection = class(TPyDistributionCollection); @@ -111,20 +118,28 @@ TPyCustomEmbeddedEnvironment = class(TPyEnvironment) [ComponentPlatforms(pidAllPlatforms)] TPyEmbeddedEnvironment = class(TPyCustomEmbeddedEnvironment) private type + TScanRule = (srFolder, srFileName); TScanner = class(TPersistent) private FAutoScan: boolean; + FScanRule: TScanRule; FEmbeddablesPath: string; FEnvironmentPath: string; + FDeleteEmbeddable: boolean; public - procedure Scan(ACallback: TProc); + procedure Scan(const AEmbedabblesPath: string; ACallback: TProc); published property AutoScan: boolean read FAutoScan write FAutoScan default false; + property ScanRule: TScanRule read FScanRule write FScanRule; property EmbeddablesPath: string read FEmbeddablesPath write FEmbeddablesPath; /// /// Default environment path. /// property EnvironmentPath: string read FEnvironmentPath write FEnvironmentPath; + /// + /// Delete the embeddable zip file after install. + /// + property DeleteEmbeddable: boolean read FDeleteEmbeddable write FDeleteEmbeddable; end; private FScanner: TScanner; @@ -148,7 +163,8 @@ implementation System.IOUtils, System.Character, System.StrUtils, PyEnvironment.Path, PyTools.ExecCmd, - PyEnvironment.Notification + PyEnvironment.Notification, + PyEnvironment.Project {$IFDEF POSIX} , Posix.SysStat, Posix.Stdlib, Posix.String_, Posix.Errno {$ENDIF} @@ -191,6 +207,7 @@ function TPyCustomEmbeddableDistribution.FileIsExecutable( const AFilePath: string): boolean; begin {$WARN SYMBOL_PLATFORM OFF} + //Avoiding symlinks Result := (TFileAttribute.faOwnerExecute in TFile.GetAttributes(AFilePath)) or (TFileAttribute.faGroupExecute in TFile.GetAttributes(AFilePath)) or (TFileAttribute.faOthersExecute in TFile.GetAttributes(AFilePath)); @@ -199,55 +216,77 @@ function TPyCustomEmbeddableDistribution.FileIsExecutable( {$ENDIF POSIX} function TPyCustomEmbeddableDistribution.FindExecutable: string; -var - LFiles: TArray; + + function DoSearch(const APath: string): TArray; {$IFDEF POSIX} - LFile: string; + var + LFile: string; {$ENDIF POSIX} + begin + Result := TDirectory.GetFiles(APath, 'python*', TSearchOption.soTopDirectoryOnly, + function(const Path: string; const SearchRec: TSearchRec): boolean + begin + Result := Char.IsDigit(SearchRec.Name, Length(SearchRec.Name) - 1); + end); + + {$IFDEF POSIX} + for LFile in Result do begin + if (TPath.GetFileName(LFile) = 'python' + PythonVersion) and (FileIsExecutable(LFile)) then + Exit(TArray.Create(LFile)); + end; + {$ENDIF POSIX} + end; +{$IFNDEF MSWINDOWS} +var + LFiles: TArray; +{$ENDIF} begin - LFiles := []; {$IFDEF MSWINDOWS} Result := TPath.Combine(GetEnvironmentPath(), 'python.exe'); if not TFile.Exists(Result) then - Result := String.Empty; + Exit(String.Empty); + {$ELSEIF DEFINED(ANDROID)} + //Let's try it in the library path first - we should place it in the library path in Android + Result := TPath.GetLibraryPath(); + LFiles := DoSearch(Result); + if LFiles <> nil then + Exit(LFiles[Low(LFiles)]); + Result := TPath.Combine(GetEnvironmentPath(), 'bin'); {$ELSE} Result := TPath.Combine(GetEnvironmentPath(), 'bin'); - LFiles := TDirectory.GetFiles(Result, 'python*', TSearchOption.soTopDirectoryOnly, - function(const Path: string; const SearchRec: TSearchRec): boolean - begin - Result := Char.IsDigit(SearchRec.Name, Length(SearchRec.Name) - 1); - end); - - {$IFDEF POSIX} - for LFile in LFiles do begin - if (TPath.GetFileName(LFile) = 'python' + PythonVersion) and (FileIsExecutable(LFile)) then - Exit(LFile); - end; - {$ENDIF POSIX} + {$ENDIF} - {$WARN SYMBOL_PLATFORM OFF} + LFiles := DoSearch(Result); if Length(LFiles) > 0 then begin - Result := LFiles[High(LFiles)]; - if (TFileAttribute.faOwnerExecute in TFile.GetAttributes(Result)) - or (TFileAttribute.faGroupExecute in TFile.GetAttributes(Result)) - or (TFileAttribute.faOthersExecute in TFile.GetAttributes(Result)) then //Avoiding symlinks - Exit; - {$WARN SYMBOL_PLATFORM ON} - Result := LFiles[Low(LFiles)]; if not TFile.Exists(Result) then Result := String.Empty; end else Result := String.Empty; - {$ENDIF} end; function TPyCustomEmbeddableDistribution.FindSharedLibrary: string; + + function DoSearch(const ALibName: string; const APath: string): TArray; + var + LFile: string; + LSearch: string; + begin + LFile := TPath.Combine(APath, ALibName); + if not TFile.Exists(LFile) then begin + LSearch := ALibName.Replace(TPath.GetExtension(ALibName), '') + '*' + TPath.GetExtension(ALibName); + Result := TDirectory.GetFiles( + APath, + LSearch, //Python <= 3.7 might contain a "m" as a sufix. + TSearchOption.soTopDirectoryOnly); + end else + Result := [LFile]; + end; + var I: integer; LLibName: string; LPath: string; - LSearch: string; LFiles: TArray; begin for I := Low(PYTHON_KNOWN_VERSIONS) to High(PYTHON_KNOWN_VERSIONS) do @@ -258,22 +297,23 @@ function TPyCustomEmbeddableDistribution.FindSharedLibrary: string; {$IFDEF MSWINDOWS} LPath := GetEnvironmentPath(); + {$ELSEIF DEFINED(ANDROID)} + //Let's try it in the library path first - we should place it in the library path in Android + LPath := TPath.GetLibraryPath(); + LFiles := DoSearch(LLibName, LPath); + if LFiles <> nil then + Exit(LFiles[Low(LFiles)]); + //Try to find it in the environments path + LPath := TPath.Combine(GetEnvironmentPath(), 'lib'); {$ELSE} LPath := TPath.Combine(GetEnvironmentPath(), 'lib'); {$ENDIF} - Result := TPath.Combine(LPath, LLibName); - if not TFile.Exists(Result) then begin - LSearch := LLibName.Replace(TPath.GetExtension(LLibName), '') + '*' + TPath.GetExtension(LLibName); - LFiles := TDirectory.GetFiles( - LPath, - LSearch, //Python <= 3.7 might contain a "m" as a sufix. - TSearchOption.soTopDirectoryOnly); - if Length(LFiles) > 0 then begin - Result := LFiles[Low(LFiles)]; - end else - Result := String.Empty; - end; + LFiles := DoSearch(LLibName, LPath); + if LFiles <> nil then + Result := LFiles[Low(LFiles)] + else + Result := String.Empty; {$IFDEF LINUX} if TFile.Exists(Result + '.1.0') then //Targets directly to the so file instead of a symlink. @@ -311,18 +351,35 @@ procedure TPyCustomEmbeddableDistribution.Setup; { TPyEmbeddableDistribution } +procedure TPyEmbeddableDistribution.DoDeleteEmbeddable; +begin + TFile.Delete(EmbeddablePackage); +end; + procedure TPyEmbeddableDistribution.LoadSettings; begin if FScanned then inherited; end; +procedure TPyEmbeddableDistribution.Setup; +begin + inherited; + if FDeleteEmbeddable and EmbeddableExists() then + DoDeleteEmbeddable(); +end; + { TPyEmbeddedEnvironment } constructor TPyEmbeddedEnvironment.Create(AOwner: TComponent); begin inherited; FScanner := TScanner.Create(); + PythonVersion := PythonProject.PythonVersion; + if PythonProject.Enabled then + FScanner.ScanRule := TScanRule.srFileName + else + FScanner.ScanRule := TScanRule.srFolder; end; destructor TPyEmbeddedEnvironment.Destroy; @@ -337,11 +394,19 @@ function TPyEmbeddedEnvironment.CreateCollection: TPyDistributionCollection; end; procedure TPyEmbeddedEnvironment.Prepare; -var +var LDistribution: TPyEmbeddableDistribution; begin if FScanner.AutoScan then begin + if PythonProject.Enabled then + if FScanner.EmbeddablesPath.IsEmpty() then begin + FScanner.EmbeddablesPath := TPyEnvironmentPath.DEPLOY_PATH; + FScanner.ScanRule := TScanRule.srFileName; + FScanner.DeleteEmbeddable := true; + end; + FScanner.Scan( + TPyEnvironmentPath.ResolvePath(FScanner.EmbeddablesPath), procedure(APyVersionInfo: TPythonVersionProp; AEmbeddablePackage: string) begin if Assigned(Distributions.LocateEnvironment(APyVersionInfo.RegVersion)) then Exit; @@ -355,7 +420,11 @@ procedure TPyEmbeddedEnvironment.Prepare; APyVersionInfo.RegVersion); LDistribution.EmbeddablePackage := AEmbeddablePackage; LDistribution.OnZipProgress := FOnZipProgress; + LDistribution.DeleteEmbeddable := FScanner.DeleteEmbeddable; end); + + if PythonVersion.IsEmpty() and (Distributions.Count > 0) then + PythonVersion := TPyEmbeddableDistribution(Distributions.Items[0]).PythonVersion; end; inherited; end; @@ -367,29 +436,46 @@ procedure TPyEmbeddedEnvironment.SetScanner(const Value: TScanner); { TPyEmbeddedEnvironment.TScanner } -procedure TPyEmbeddedEnvironment.TScanner.Scan( +procedure TPyEmbeddedEnvironment.TScanner.Scan(const AEmbedabblesPath: string; ACallback: TProc); var I: Integer; LPath: string; LFiles: TArray; + LPythonVersion: string; + LSearchPatter: string; begin if not Assigned(ACallback) then Exit; - if not TDirectory.Exists(FEmbeddablesPath) then + if not TDirectory.Exists(AEmbedabblesPath) then raise Exception.Create('Directory not found.'); - for I := Low(PYTHON_KNOWN_VERSIONS) to High(PYTHON_KNOWN_VERSIONS) do begin - LPath := TPath.Combine(FEmbeddablesPath, PYTHON_KNOWN_VERSIONS[I].RegVersion); - if not TDirectory.Exists(LPath) then - Continue; + //Look for version named subfolders + if (FScanRule = TScanRule.srFolder) then begin + LSearchPatter := '*.zip'; + for I := Low(PYTHON_KNOWN_VERSIONS) to High(PYTHON_KNOWN_VERSIONS) do begin + LPath := TPath.Combine(AEmbedabblesPath, PYTHON_KNOWN_VERSIONS[I].RegVersion); + if not TDirectory.Exists(LPath) then + Continue; - LFiles := TDirectory.GetFiles(LPath, '*.zip', TSearchOption.soTopDirectoryOnly); - if (Length(LFiles) = 0) then - Continue; + LFiles := TDirectory.GetFiles(LPath, LSearchPatter, TSearchOption.soTopDirectoryOnly); + if (Length(LFiles) = 0) then + Continue; - ACallback(PYTHON_KNOWN_VERSIONS[I], LFiles[0]); + ACallback(PYTHON_KNOWN_VERSIONS[I], LFiles[0]); + end; + end else if (FScanRule = TScanRule.srFileName) then begin + //Look for pattern named files + for I := Low(PYTHON_KNOWN_VERSIONS) to High(PYTHON_KNOWN_VERSIONS) do begin + LPythonVersion := PYTHON_KNOWN_VERSIONS[I].RegVersion; + LSearchPatter := Format('python3-*-%s*.zip', [LPythonVersion]); + LFiles := TDirectory.GetFiles(AEmbedabblesPath, LSearchPatter, TSearchOption.soTopDirectoryOnly); + if (Length(LFiles) = 0) then + Continue; + + ACallback(PYTHON_KNOWN_VERSIONS[I], LFiles[0]); + end end; end; diff --git a/src/Project/IDE/PyEnvironment.Project.IDE.Deploy.pas b/src/Project/IDE/PyEnvironment.Project.IDE.Deploy.pas new file mode 100644 index 0000000..f606d0c --- /dev/null +++ b/src/Project/IDE/PyEnvironment.Project.IDE.Deploy.pas @@ -0,0 +1,188 @@ +unit PyEnvironment.Project.IDE.Deploy; + +interface + +uses + System.Generics.Collections, + DeploymentAPI, + PyEnvironment.Project.IDE.Types; + +type + TPyEnvironmentProjectDeploy = class + strict private + class var + FDeployableFiles: TDictionary>; + FAbsolutePath: string; + FPath: string; + FPathChecked: Boolean; + class procedure FindPath(out APath, AAbsolutePath: string); static; + class function GetAbsolutePath: string; static; + class function GetFound: Boolean; static; + class function GetPath: string; static; + class function IsValidPythonEnvironmentDir(const APath: string): Boolean; static; + public + const + DEPLOYMENT_CLASS = 'Python'; + PROJECT_USE_PYTHON = 'PYTHON'; + PROJECT_NO_USE_PYTHON = 'NOPYTHON'; + PYTHON_ENVIRONMENT_DIR_VARIABLE = 'PYTHONENVIRONMENTDIR'; + PYTHON_VERSIONS: array[0..3] of string = ('3.7', '3.8', '3.9', '3.10'); + SUPPORTED_PLATFORMS = [ + TPyEnvironmentProjectPlatform.Win32, TPyEnvironmentProjectPlatform.Win64, + TPyEnvironmentProjectPlatform.Android, TPyEnvironmentProjectPlatform.Android64, + //TPyEnvironmentProjectPlatform.iOSDevice64, + TPyEnvironmentProjectPlatform.OSX64, TPyEnvironmentProjectPlatform.OSXARM64, + TPyEnvironmentProjectPlatform.Linux64]; + public + class constructor Create(); + class destructor Destroy(); + + class function GetDeployFiles(const APythonVersion: string; const APlatform: TPyEnvironmentProjectPlatform): TArray; static; + class property AbsolutePath: string read GetAbsolutePath; + class property Found: Boolean read GetFound; + class property Path: string read GetPath; + end; + +implementation + +uses + System.SysUtils, System.IOUtils, + PyEnvironment.Project.IDE.Helper; + +{ TPyEnvironmentProject } + +class constructor TPyEnvironmentProjectDeploy.Create; +begin + FDeployableFiles := TDictionary>.Create(); + + FDeployableFiles.Add('3.7', [ + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Win32, 'python\python3-windows-3.7.9-win32.zip', '.\', True, True, TDeployOperation.doCopyOnly, ''), // Win32 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Win64, 'python\python3-windows-3.7.9-amd64.zip', '.\', True, True, TDeployOperation.doCopyOnly, ''), // Win64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\python3-android-3.7.13-arm.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\python3-android-3.7.13-arm64.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\python3-android-3.7.13-arm.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\android\3.7.13\arm\libpython3.7m.so', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.7.13\arm64\libpython3.7m.so', 'library\lib\arm64-v8a\', False, True, TDeployOperation.doSetExecBit, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.7.13\arm\libpython3.7m.so', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\android\3.7.13\arm\python3.7', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.7.13\arm64\python3.7', 'library\lib\arm64-v8a\', False, True, TDeployOperation.doSetExecBit, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.7.13\arm\python3.7', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.OSX64, 'python\python3-macos-3.7.13-x86_64.zip', 'Contents\MacOS\', True, True, TDeployOperation.doCopyOnly, ''), // OSX64 + //TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.OSXARM64, 'python\python3-macos-3.7.13-universal2.zip', 'Contents\MacOS\', True, True, TDeployOperation.doCopyOnly, ''), // OSXARM64 //3.7 is not available for M1 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Linux64, 'python\python3-linux-3.7.13-x86_64.zip', '.\', True, True, TDeployOperation.doCopyOnly, '') // Linux64 + ]); + + FDeployableFiles.Add('3.8', [ + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Win32, 'python\python3-windows-3.8.10-win32.zip', '.\', True, True, TDeployOperation.doCopyOnly, ''), // Win32 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Win64, 'python\python3-windows-3.8.10-amd64.zip', '.\', True, True, TDeployOperation.doCopyOnly, ''), // Win64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\python3-android-3.8.13-arm.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\python3-android-3.8.13-arm64.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\python3-android-3.8.13-arm.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\android\3.8.13\arm\libpython3.8.so', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.8.13\arm64\libpython3.8.so', 'library\lib\arm64-v8a\', False, True, TDeployOperation.doSetExecBit, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.8.13\arm\libpython3.8.so', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\android\3.8.13\arm\python3.8', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.8.13\arm64\python3.8', 'library\lib\arm64-v8a\', False, True, TDeployOperation.doSetExecBit, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.8.13\arm\python3.8', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.OSX64, 'python\python3-macos-3.8.13-x86_64.zip', 'Contents\MacOS\', True, True, TDeployOperation.doCopyOnly, ''), // OSX64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.OSXARM64, 'python\python3-macos-3.8.13-universal2.zip', 'Contents\MacOS\', True, True, TDeployOperation.doCopyOnly, ''), // OSXARM64 //3.7 is not available for M1 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Linux64, 'python\python3-linux-3.8.13-x86_64.zip', '.\', True, True, TDeployOperation.doCopyOnly, '') // Linux64 + ]); + + FDeployableFiles.Add('3.9', [ + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Win32, 'python\python3-windows-3.9.12-win32.zip', '.\', True, True, TDeployOperation.doCopyOnly, ''), // Win32 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Win64, 'python\python3-windows-3.9.12-amd64.zip', '.\', True, True, TDeployOperation.doCopyOnly, ''), // Win64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\python3-android-3.9.12-arm.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\python3-android-3.9.12-arm64.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\python3-android-3.9.12-arm.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\android\3.9.12\arm\libpython3.9.so', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.9.12\arm64\libpython3.9.so', 'library\lib\arm64-v8a\', False, True, TDeployOperation.doSetExecBit, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.9.12\arm\libpython3.9.so', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\android\3.9.12\arm\python3.9', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.9.12\arm64\python3.9', 'library\lib\arm64-v8a\', False, True, TDeployOperation.doSetExecBit, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.9.12\arm\python3.9', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.OSX64, 'python\python3-macos-3.9.12-x86_64.zip', 'Contents\MacOS\', True, True, TDeployOperation.doCopyOnly, ''), // OSX64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.OSXARM64, 'python\python3-macos-3.9.12-universal2.zip', 'Contents\MacOS\', True, True, TDeployOperation.doCopyOnly, ''), // OSXARM64 //3.7 is not available for M1 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Linux64, 'python\python3-linux-3.9.12-x86_64.zip', '.\', True, True, TDeployOperation.doCopyOnly, '') // Linux64 + ]); + + FDeployableFiles.Add('3.10', [ + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Win32, 'python\python3-windows-3.10.4-win32.zip', '.\', True, True, TDeployOperation.doCopyOnly, ''), // Win32 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Win64, 'python\python3-windows-3.10.4-amd64.zip', '.\', True, True, TDeployOperation.doCopyOnly, ''), // Win64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\python3-android-3.10.4-arm.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\python3-android-3.10.4-arm64.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\python3-android-3.10.4-arm.zip', '.\assets\internal', False, True, TDeployOperation.doCopyOnly, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\android\3.10.4\arm\libpython3.10.so', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.10.4\arm64\libpython3.10.so', 'library\lib\arm64-v8a\', False, True, TDeployOperation.doSetExecBit, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.10.4\arm\libpython3.10.so', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android, 'python\android\3.10.4\arm\python3.10', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, ''), // Android + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.10.4\arm64\python3.10', 'library\lib\arm64-v8a\', False, True, TDeployOperation.doSetExecBit, ''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Android64, 'python\android\3.10.4\arm\python3.10', 'library\lib\armeabi-v7a\', False, True, TDeployOperation.doSetExecBit, '''$(AndroidAppBundle)''==''true'''), // Android64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.OSX64, 'python\python3-macos-3.10.4-x86_64.zip', 'Contents\MacOS\', True, True, TDeployOperation.doCopyOnly, ''), // OSX64 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.OSXARM64, 'python\python3-macos-3.10.4-universal2.zip', 'Contents\MacOS\', True, True, TDeployOperation.doCopyOnly, ''), // OSXARM64 //3.7 is not available for M1 + TPyEnvironmentDeployFile.Create(TPyEnvironmentProjectPlatform.Linux64, 'python\python3-linux-3.10.4-x86_64.zip', '.\', True, True, TDeployOperation.doCopyOnly, '') // Linux64 + ]); +end; + +class destructor TPyEnvironmentProjectDeploy.Destroy; +begin + FDeployableFiles.Free(); +end; + +class procedure TPyEnvironmentProjectDeploy.FindPath(out APath, + AAbsolutePath: string); +begin + AAbsolutePath := TPyEnvironmentOTAHelper.GetEnvironmentVar(PYTHON_ENVIRONMENT_DIR_VARIABLE, True); + if IsValidPythonEnvironmentDir(AAbsolutePath) then + APath := '$(' + PYTHON_ENVIRONMENT_DIR_VARIABLE + ')' + else begin + APath := ''; + AAbsolutePath := ''; + end; +end; + +class function TPyEnvironmentProjectDeploy.GetAbsolutePath: string; +begin + if not FPathChecked then + GetPath(); + Result := FAbsolutePath; +end; + +class function TPyEnvironmentProjectDeploy.GetDeployFiles(const APythonVersion: string; + const APlatform: TPyEnvironmentProjectPlatform): TArray; +var + I: Integer; + LAllFiles: TArray; +begin + Result := []; + + if not FDeployableFiles.ContainsKey(APythonVersion) then + Exit(); + + LAllFiles := FDeployableFiles[APythonVersion]; + for I := Low(LAllFiles) to High(LAllFiles) do + if LAllFiles[I].Platform = APlatform then + Result := Result + [LAllFiles[I]]; +end; + +class function TPyEnvironmentProjectDeploy.GetFound: Boolean; +begin + Result := not Path.IsEmpty; +end; + +class function TPyEnvironmentProjectDeploy.GetPath: string; +begin + if not FPathChecked then begin + FindPath(FPath, FAbsolutePath); + FPathChecked := True; + end; + Result := FPath; +end; + +class function TPyEnvironmentProjectDeploy.IsValidPythonEnvironmentDir( + const APath: string): Boolean; +begin + Result := TDirectory.Exists(APath); +end; + +end. diff --git a/src/Project/IDE/PyEnvironment.Project.IDE.Helper.pas b/src/Project/IDE/PyEnvironment.Project.IDE.Helper.pas new file mode 100644 index 0000000..bd47583 --- /dev/null +++ b/src/Project/IDE/PyEnvironment.Project.IDE.Helper.pas @@ -0,0 +1,772 @@ +unit PyEnvironment.Project.IDE.Helper; + +interface + +uses + System.Classes, + ToolsAPI, DeploymentAPI, + PyEnvironment.Project.IDE.Types; + +type + TPyEnvironmentProjectHelper = record + strict private + const + PYTHON_VERSION_PREFIX = 'PYTHONVER'; + class function ContainsStringInArray(const AString: string; const AArray: TArray): Boolean; static; inline; + class function GetIsPyEnvironmentDefined( + const AProject: IOTAProject): Boolean; static; + class procedure SetIsPyEnvironmentDefined(const AProject: IOTAProject; + const AValue: Boolean); static; + class function GetCurrentPythonVersion( + const AProject: IOTAProject): string; static; + class procedure SetCurrentPythonVersion(const AProject: IOTAProject; + const AValue: string); static; + class function BuildPythonVersionConditional(const APythonVersion: string): string; static; + public + class procedure AddDeployFile(const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; const ADeployFile: TPyEnvironmentDeployFile); static; + class procedure RemoveDeployFile(const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; const APlatform: TPyEnvironmentProjectPlatform; ALocalFileName: string; const ARemoteDir: string); static; + class procedure RemoveDeployFilesOfClass(const AProject: IOTAProject); overload; static; + class procedure RemoveDeployFilesOfClass(const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; const APlatform: TPyEnvironmentProjectPlatform); overload; static; + class procedure RemoveUnexpectedDeployFilesOfClass(const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; const APlatform: TPyEnvironmentProjectPlatform; const AAllowedFiles: TArray); static; + + class function IsPyEnvironmentDefinedForPlatform(const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; const AConfig: TPyEnvironmentProjectConfig): Boolean; static; + class function SupportsEnvironmentDeployment(const AProject: IOTAProject): Boolean; static; + + class property IsPyEnvironmentDefined[const AProject: IOTAProject]: Boolean read GetIsPyEnvironmentDefined write SetIsPyEnvironmentDefined; + class property CurrentPythonVersion[const AProject: IOTAProject]: string read GetCurrentPythonVersion write SetCurrentPythonVersion; + end; + + TPyEnvironmentOTAHelper = record + strict private + const + DefaultOptionsSeparator = ';'; + OutputDirPropertyName = 'OutputDir'; + class function ExpandConfiguration(const ASource: string; const AConfig: IOTABuildConfiguration): string; static; + class function ExpandEnvironmentVar(var AValue: string): Boolean; static; + class function ExpandOutputPath(const ASource: string; const ABuildConfig: IOTABuildConfiguration): string; static; + class function ExpandPath(const ABaseDir, ARelativeDir: string): string; static; + class function ExpandVars(const ASource: string): string; static; + class function GetEnvironmentVars(const AVars: TStrings; AExpand: Boolean): Boolean; static; + class function GetProjectOptionsConfigurations(const AProject: IOTAProject): IOTAProjectOptionsConfigurations; static; + class procedure MultiSzToStrings(const ADest: TStrings; const ASource: PChar); static; + class procedure StrResetLength(var S: string); static; + class function TryGetProjectOutputPath(const AProject: IOTAProject; ABuildConfig: IOTABuildConfiguration; out AOutputPath: string): Boolean; overload; static; + public + class function ContainsOptionValue(const AValues, AValue: string; const ASeparator: string = DefaultOptionsSeparator): Boolean; static; + class function InsertOptionValue(const AValues, AValue: string; const ASeparator: string = DefaultOptionsSeparator): string; static; + class function RemoveOptionValue(const AValues, AValue: string; const ASeparator: string = DefaultOptionsSeparator): string; static; + + class function GetEnvironmentVar(const AName: string; AExpand: Boolean): string; static; + class function TryCopyFileToOutputPath(const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; const AConfig: TPyEnvironmentProjectConfig; const AFileName: string): Boolean; static; + class function TryCopyFileToOutputPathOfActiveBuild(const AProject: IOTAProject; const AFileName: string): Boolean; static; + class function TryGetBuildConfig(const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; const AConfig: TPyEnvironmentProjectConfig; out ABuildConfig: IOTABuildConfiguration): Boolean; static; + class function TryGetProjectOutputPath(const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; const AConfig: TPyEnvironmentProjectConfig; out AOutputPath: string): Boolean; overload; static; + class function TryGetProjectOutputPathOfActiveBuild(const AProject: IOTAProject; out AOutputPath: string): Boolean; static; + class function TryRemoveOutputFile(const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; const AConfig: TPyEnvironmentProjectConfig; AFileName: string): Boolean; static; + class function TryRemoveOutputFileOfActiveBuild(const AProject: IOTAProject; const AFileName: string): Boolean; static; + end; + +implementation + +uses + System.SysUtils, System.IOUtils, System.Generics.Collections, + DccStrs, + Winapi.Windows, Winapi.ShLwApi, + PyEnvironment.Project.IDE.Deploy; + +{ TPyEnvironmentProjectHelper } + +class procedure TPyEnvironmentProjectHelper.AddDeployFile( + const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; + const ADeployFile: TPyEnvironmentDeployFile); +type + TDeployFileExistence = (DoesNotExist, AlreadyExists, NeedReplaced); + + function GetDeployFileExistence(const AProjectDeployment: IProjectDeployment; + const ALocalFileName, ARemoteDir, APlatformName, AConfigName: string): TDeployFileExistence; + var + LRemoteFileName: string; + LFile: IProjectDeploymentFile; + LFiles: TDictionary.TValueCollection; + begin + Result := TDeployFileExistence.DoesNotExist; + LRemoteFileName := TPath.Combine(ARemoteDir, TPath.GetFileName(ALocalFileName)); + LFiles := AProjectDeployment.Files; + if Assigned(LFiles) then begin + for LFile in LFiles do begin + if (LFile.FilePlatform = APlatformName) and (LFile.Configuration = AConfigName) then begin + if SameText(LRemoteFileName, TPath.Combine(LFile.RemoteDir[APlatformName], LFile.RemoteName[APlatformName])) then begin + if (LFile.LocalName = ALocalFileName) and (LFile.DeploymentClass = TPyEnvironmentProjectDeploy.DEPLOYMENT_CLASS) and + (LFile.Condition = ADeployFile.Condition) and (LFile.Operation[APlatformName] = ADeployFile.Operation) and + LFile.Enabled[APlatformName] and LFile.Overwrite[APlatformName] and + (LFile.Required = ADeployFile.Required) and (Result = TDeployFileExistence.DoesNotExist) then + begin + Result := TDeployFileExistence.AlreadyExists; + end else + Exit(TDeployFileExistence.NeedReplaced); + end; + end; + end; + end; + end; + + function CanDeployFile(const ADeployFileExistence: TDeployFileExistence): boolean; + begin + Result := (ADeployFileExistence in [TDeployFileExistence.NeedReplaced, + TDeployFileExistence.DoesNotExist]) + and (AConfig in ADeployFile.Configs) + end; + + procedure DoAddDeployFile(const AProjectDeployment: IProjectDeployment; + const ALocalFileName, APlatformName, AConfigName: string); + var + LFile: IProjectDeploymentFile; + begin + LFile := AProjectDeployment.CreateFile(AConfigName, APlatformName, ALocalFileName); + if Assigned(LFile) then begin + LFile.Overwrite[APlatformName] := True; + LFile.Enabled[APlatformName] := True; + LFile.Required := ADeployFile.Required; + LFile.Condition := ADeployFile.Condition; + LFile.Operation[APlatformName] := ADeployFile.Operation; + LFile.RemoteDir[APlatformName] := ADeployFile.RemotePath; + LFile.DeploymentClass := TPyEnvironmentProjectDeploy.DEPLOYMENT_CLASS; + LFile.RemoteName[APlatformName] := TPath.GetFileName(ALocalFileName); + AProjectDeployment.AddFile(AConfigName, APlatformName, LFile); + end; + end; + +var + LProjectDeployment: IProjectDeployment; + LConfigName: string; + LPlatformName: string; + LLocalFileName: string; + LDeployFileExistence: TDeployFileExistence; +begin + if (ADeployFile.LocalFileName <> '') and Supports(AProject, IProjectDeployment, LProjectDeployment) then begin + LConfigName := AConfig.ToString; + LPlatformName := ADeployFile.Platform.ToString; + LLocalFileName := TPath.Combine(TPyEnvironmentProjectDeploy.Path, ADeployFile.LocalFileName); + LDeployFileExistence := GetDeployFileExistence(LProjectDeployment, LLocalFileName, ADeployFile.RemotePath, LPlatformName, LConfigName); + if LDeployFileExistence = TDeployFileExistence.NeedReplaced then + RemoveDeployFile(AProject, AConfig, ADeployFile.Platform, ADeployFile.LocalFileName, ADeployFile.RemotePath); + if CanDeployFile(LDeployFileExistence) then + DoAddDeployFile(LProjectDeployment, LLocalFileName, LPlatformName, LConfigName); + end; +end; + +class function TPyEnvironmentProjectHelper.BuildPythonVersionConditional( + const APythonVersion: string): string; +begin + Result := PYTHON_VERSION_PREFIX + APythonVersion.Replace('.', '', [rfReplaceAll]); +end; + +class function TPyEnvironmentProjectHelper.ContainsStringInArray( + const AString: string; const AArray: TArray): Boolean; +var + I: Integer; +begin + Result := False; + for I := Low(AArray) to High(AArray) do + if AArray[I] = AString then + Exit(True); +end; + +class function TPyEnvironmentProjectHelper.GetCurrentPythonVersion( + const AProject: IOTAProject): string; +var + LBaseConfiguration: IOTABuildConfiguration; + LOptionsConfigurations: IOTAProjectOptionsConfigurations; + LPythonVersion: string; +begin + if not (Assigned(AProject) + and Supports(AProject.ProjectOptions, IOTAProjectOptionsConfigurations, LOptionsConfigurations) + and Assigned(LOptionsConfigurations.BaseConfiguration)) then + Exit(String.Empty); + + LBaseConfiguration := LOptionsConfigurations.BaseConfiguration; + if Assigned(LBaseConfiguration) then begin + for LPythonVersion in TPyEnvironmentProjectDeploy.PYTHON_VERSIONS do begin + if TPyEnvironmentOTAHelper.ContainsOptionValue( + LBaseConfiguration.Value[sDefine], + BuildPythonVersionConditional(LPythonVersion)) then + Exit(LPythonVersion); + end; + end; + + Result := String.Empty; +end; + +class function TPyEnvironmentProjectHelper.GetIsPyEnvironmentDefined( + const AProject: IOTAProject): Boolean; +var + LBaseConfiguration: IOTABuildConfiguration; + LOptionsConfigurations: IOTAProjectOptionsConfigurations; +begin + Result := Assigned(AProject) + and Supports(AProject.ProjectOptions, IOTAProjectOptionsConfigurations, LOptionsConfigurations); + + if Result then begin + LBaseConfiguration := LOptionsConfigurations.BaseConfiguration; + Result := Assigned(LBaseConfiguration) + and TPyEnvironmentOTAHelper.ContainsOptionValue( + LBaseConfiguration.Value[sDefine], TPyEnvironmentProjectDeploy.PROJECT_USE_PYTHON); + end; +end; + +class function TPyEnvironmentProjectHelper.IsPyEnvironmentDefinedForPlatform( + const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; + const AConfig: TPyEnvironmentProjectConfig): Boolean; +var + LBuildConfig: IOTABuildConfiguration; +begin + Assert(IsPyEnvironmentDefined[AProject]); + Result := TPyEnvironmentOTAHelper.TryGetBuildConfig( + AProject, APlatform, AConfig, LBuildConfig) + and not TPyEnvironmentOTAHelper.ContainsOptionValue( + LBuildConfig.Value[sDefine], TPyEnvironmentProjectDeploy.PROJECT_NO_USE_PYTHON); +end; + +class procedure TPyEnvironmentProjectHelper.RemoveDeployFile( + const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; + const APlatform: TPyEnvironmentProjectPlatform; ALocalFileName: string; + const ARemoteDir: string); +var + LProjectDeployment: IProjectDeployment; + LFiles: TDictionary.TValueCollection; + LFile: IProjectDeploymentFile; + LRemoteFileName: string; + LRemoveFiles: TArray; +begin + if (ALocalFileName <> '') and Supports(AProject, IProjectDeployment, LProjectDeployment) then begin + ALocalFileName := TPath.Combine(TPyEnvironmentProjectDeploy.Path, ALocalFileName); + LProjectDeployment.RemoveFile(AConfig.ToString, APlatform.ToString, ALocalFileName); + LFiles := LProjectDeployment.Files; + if Assigned(LFiles) then begin + LRemoteFileName := TPath.Combine(ARemoteDir, TPath.GetFileName(ALocalFileName)); + LRemoveFiles := []; + for LFile in LFiles do + if SameText(LRemoteFileName, TPath.Combine(LFile.RemoteDir[APlatform.ToString], LFile.RemoteName[APlatform.ToString])) then + LRemoveFiles := LRemoveFiles + [LFile]; + for LFile in LRemoveFiles do + LProjectDeployment.RemoveFile(AConfig.ToString, APlatform.ToString, LFile.LocalName); + end; + end; +end; + +class procedure TPyEnvironmentProjectHelper.RemoveDeployFilesOfClass( + const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; + const APlatform: TPyEnvironmentProjectPlatform); +var + LProjectDeployment: IProjectDeployment; + LFile: IProjectDeploymentFile; + LConfigName: string; + LPlatformName: string; +begin + if Supports(AProject, IProjectDeployment, LProjectDeployment) then begin + LConfigName := AConfig.ToString; + LPlatformName := APlatform.ToString; + for LFile in LProjectDeployment.GetFilesOfClass(TPyEnvironmentProjectDeploy.DEPLOYMENT_CLASS) do + if (LFile.Configuration = LConfigName) and ContainsStringInArray(LPlatformName, LFile.Platforms) then + LProjectDeployment.RemoveFile(LConfigName, LPlatformName, LFile.LocalName); + end; +end; + +class procedure TPyEnvironmentProjectHelper.RemoveUnexpectedDeployFilesOfClass( + const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; + const APlatform: TPyEnvironmentProjectPlatform; + const AAllowedFiles: TArray); + + function IsAllowedFile(const AFile: IProjectDeploymentFile; const APlatformName: string): Boolean; + var + LDeployFile: TPyEnvironmentDeployFile; + begin + Result := False; + for LDeployFile in AAllowedFiles do begin + if (AFile.LocalName = LDeployFile.LocalFileName) and SameText(AFile.RemoteDir[APlatformName], LDeployFile.RemotePath) and + SameText(AFile.RemoteName[APlatformName], TPath.GetFileName(LDeployFile.LocalFileName)) and + (AFile.DeploymentClass = TPyEnvironmentProjectDeploy.DEPLOYMENT_CLASS) and + (AFile.Condition = LDeployFile.Condition) and (AFile.Operation[APlatformName] = LDeployFile.Operation) and + AFile.Enabled[APlatformName] and AFile.Overwrite[APlatformName] and + (AFile.Required = LDeployFile.Required) then + begin + Exit(True); + end; + end; + end; + +var + LProjectDeployment: IProjectDeployment; + LFile: IProjectDeploymentFile; + LConfigName: string; + LPlatformName: string; +begin + if Supports(AProject, IProjectDeployment, LProjectDeployment) then begin + LConfigName := AConfig.ToString; + LPlatformName := APlatform.ToString; + for LFile in LProjectDeployment.GetFilesOfClass(TPyEnvironmentProjectDeploy.DEPLOYMENT_CLASS) do begin + if (LFile.Configuration = LConfigName) and ContainsStringInArray(LPlatformName, LFile.Platforms) and + not IsAllowedFile(LFile, LPlatformName) then + begin + LProjectDeployment.RemoveFile(LConfigName, LPlatformName, LFile.LocalName); + end; + end; + end; +end; + +class procedure TPyEnvironmentProjectHelper.RemoveDeployFilesOfClass( + const AProject: IOTAProject); +var + LProjectDeployment: IProjectDeployment; +begin + if Supports(AProject, IProjectDeployment, LProjectDeployment) then + LProjectDeployment.RemoveFilesOfClass(TPyEnvironmentProjectDeploy.DEPLOYMENT_CLASS); +end; + +class procedure TPyEnvironmentProjectHelper.SetCurrentPythonVersion( + const AProject: IOTAProject; const AValue: string); +var + LProjectOptions: IOTAProjectOptions; + LOptionsConfigurations: IOTAProjectOptionsConfigurations; + LBaseConfiguration: IOTABuildConfiguration; + LPythonVersion: string; +begin + if Assigned(AProject) then begin + LProjectOptions := AProject.ProjectOptions; + if Assigned(LProjectOptions) then begin + if Supports(LProjectOptions, IOTAProjectOptionsConfigurations, LOptionsConfigurations) then begin + LBaseConfiguration := LOptionsConfigurations.BaseConfiguration; + if Assigned(LBaseConfiguration) then begin + LBaseConfiguration := LOptionsConfigurations.BaseConfiguration; + for LPythonVersion in TPyEnvironmentProjectDeploy.PYTHON_VERSIONS do + LBaseConfiguration.Value[sDefine] := + TPyEnvironmentOTAHelper.RemoveOptionValue( + LBaseConfiguration.Value[sDefine], + BuildPythonVersionConditional(LPythonVersion)); + + if not AValue.IsEmpty() then + LBaseConfiguration.Value[sDefine] := TPyEnvironmentOTAHelper + .InsertOptionValue(LBaseConfiguration.Value[sDefine], + BuildPythonVersionConditional(AValue)); + end; + end; + LProjectOptions.ModifiedState := True; + end; + end; +end; + +class procedure TPyEnvironmentProjectHelper.SetIsPyEnvironmentDefined( + const AProject: IOTAProject; const AValue: Boolean); +var + LProjectOptions: IOTAProjectOptions; + LOptionsConfigurations: IOTAProjectOptionsConfigurations; + LBaseConfiguration: IOTABuildConfiguration; +begin + if Assigned(AProject) then begin + LProjectOptions := AProject.ProjectOptions; + if Assigned(LProjectOptions) then begin + if Supports(LProjectOptions, IOTAProjectOptionsConfigurations, LOptionsConfigurations) then begin + LBaseConfiguration := LOptionsConfigurations.BaseConfiguration; + if Assigned(LBaseConfiguration) then begin + if AValue then + LBaseConfiguration.Value[sDefine] := TPyEnvironmentOTAHelper + .InsertOptionValue( + LBaseConfiguration.Value[sDefine], TPyEnvironmentProjectDeploy.PROJECT_USE_PYTHON) + else + LBaseConfiguration.Value[sDefine] := TPyEnvironmentOTAHelper + .RemoveOptionValue( + LBaseConfiguration.Value[sDefine], TPyEnvironmentProjectDeploy.PROJECT_USE_PYTHON); + end; + end; + LProjectOptions.ModifiedState := True; + end; + end; +end; + +class function TPyEnvironmentProjectHelper.SupportsEnvironmentDeployment( + const AProject: IOTAProject): Boolean; +begin + Result := Assigned(AProject) + and AProject.FileName.EndsWith('.dproj', True) + and ((AProject.ApplicationType = sApplication) + or + (AProject.ApplicationType = sConsole)); +end; + +{ TPyEnvironmentOTAHelper } + +class function TPyEnvironmentOTAHelper.ContainsOptionValue(const AValues, + AValue, ASeparator: string): Boolean; +var + LValues: TArray; + I: Integer; +begin + LValues := AValues.Split([ASeparator], TStringSplitOptions.None); + for I := 0 to Length(LValues) - 1 do + if SameText(LValues[I], AValue) then + Exit(True); + Result := False; +end; + +class function TPyEnvironmentOTAHelper.ExpandConfiguration( + const ASource: string; const AConfig: IOTABuildConfiguration): string; +begin + Result := StringReplace(ASource, '$(Platform)', AConfig.Platform, [rfReplaceAll, rfIgnoreCase]); + Result := StringReplace(Result, '$(Config)', AConfig.Name, [rfReplaceAll, rfIgnoreCase]); +end; + +class function TPyEnvironmentOTAHelper.ExpandEnvironmentVar( + var AValue: string): Boolean; +var + R: Integer; + LExpanded: string; +begin + SetLength(LExpanded, 1); + R := ExpandEnvironmentStrings(PChar(AValue), PChar(LExpanded), 0); + SetLength(LExpanded, R); + Result := ExpandEnvironmentStrings(PChar(AValue), PChar(LExpanded), R) <> 0; + if Result then begin + StrResetLength(LExpanded); + AValue := LExpanded; + end; +end; + +class function TPyEnvironmentOTAHelper.ExpandOutputPath(const ASource: string; + const ABuildConfig: IOTABuildConfiguration): string; +begin + if Assigned(ABuildConfig) then + Result := ExpandConfiguration(ASource, ABuildConfig) + else + Result := ASource; + Result := ExpandVars(Result); +end; + +class function TPyEnvironmentOTAHelper.ExpandPath(const ABaseDir, + ARelativeDir: string): string; +var + LBuffer: array [0..MAX_PATH - 1] of Char; +begin + if PathIsRelative(PChar(ARelativeDir)) then + Result := IncludeTrailingPathDelimiter(ABaseDir) + ARelativeDir + else + Result := ARelativeDir; + if PathCanonicalize(@LBuffer[0], PChar(Result)) then + Result := LBuffer; +end; + +class function TPyEnvironmentOTAHelper.ExpandVars( + const ASource: string): string; +var + LVars: TStrings; + I: Integer; +begin + Result := ASource; + if not Result.IsEmpty then begin + LVars := TStringList.Create; + try + GetEnvironmentVars(LVars, True); + for I := 0 to LVars.Count - 1 do begin + Result := StringReplace(Result, '$(' + LVars.Names[I] + ')', LVars.Values[LVars.Names[I]], [rfReplaceAll, rfIgnoreCase]); + Result := StringReplace(Result, '%' + LVars.Names[I] + '%', LVars.Values[LVars.Names[I]], [rfReplaceAll, rfIgnoreCase]); + end; + finally + LVars.Free; + end; + end; +end; + +class function TPyEnvironmentOTAHelper.GetEnvironmentVar(const AName: string; + AExpand: Boolean): string; +const + BufSize = 1024; +var + Len: Integer; + Buffer: array[0..BufSize - 1] of Char; + LExpanded: string; +begin + Result := ''; + Len := Winapi.Windows.GetEnvironmentVariable(PChar(AName), @Buffer, BufSize); + if Len < BufSize then + SetString(Result, PChar(@Buffer), Len) + else begin + SetLength(Result, Len - 1); + Winapi.Windows.GetEnvironmentVariable(PChar(AName), PChar(Result), Len); + end; + if AExpand then begin + LExpanded := Result; + if ExpandEnvironmentVar(LExpanded) then + Result := LExpanded; + end; +end; + +class function TPyEnvironmentOTAHelper.GetEnvironmentVars(const AVars: TStrings; + AExpand: Boolean): Boolean; +var + LRaw: PChar; + LExpanded: string; + I: Integer; +begin + AVars.BeginUpdate; + try + AVars.Clear; + LRaw := GetEnvironmentStrings; + try + MultiSzToStrings(AVars, LRaw); + Result := True; + finally + FreeEnvironmentStrings(LRaw); + end; + if AExpand then begin + for I := 0 to AVars.Count - 1 do begin + LExpanded := AVars[I]; + if ExpandEnvironmentVar(LExpanded) then + AVars[I] := LExpanded; + end; + end; + finally + AVars.EndUpdate; + end; +end; + +class function TPyEnvironmentOTAHelper.GetProjectOptionsConfigurations( + const AProject: IOTAProject): IOTAProjectOptionsConfigurations; +var + LProjectOptions: IOTAProjectOptions; +begin + Result := nil; + if AProject <> nil then begin + LProjectOptions := AProject.ProjectOptions; + if LProjectOptions <> nil then + Supports(LProjectOptions, IOTAProjectOptionsConfigurations, Result); + end; +end; + +class function TPyEnvironmentOTAHelper.InsertOptionValue(const AValues, AValue, + ASeparator: string): string; +var + LValues: TArray; + I: Integer; +begin + LValues := AValues.Split([ASeparator], TStringSplitOptions.None); + try + for I := 0 to Length(LValues) - 1 do begin + if SameText(LValues[I], AValue) then begin + LValues[I] := AValue; + Exit; + end; + end; + LValues := LValues + [AValue]; + finally + if LValues = nil then + Result := '' + else + Result := string.Join(ASeparator, LValues); + end; +end; + +class procedure TPyEnvironmentOTAHelper.MultiSzToStrings(const ADest: TStrings; + const ASource: PChar); +var + P: PChar; +begin + ADest.BeginUpdate; + try + ADest.Clear; + if ASource <> nil then begin + P := ASource; + while P^ <> #0 do begin + ADest.Add(P); + P := StrEnd(P); + Inc(P); + end; + end; + finally + ADest.EndUpdate; + end; +end; + +class function TPyEnvironmentOTAHelper.RemoveOptionValue(const AValues, AValue, + ASeparator: string): string; +var + LValues: TArray; + LNewValues: TArray; + I: Integer; +begin + LNewValues := []; + LValues := AValues.Split([ASeparator], TStringSplitOptions.None); + for I := 0 to Length(LValues) - 1 do + if not SameText(LValues[I], AValue) then + LNewValues := LNewValues + [LValues[I]]; + if LNewValues = nil then + Result := '' + else + Result := string.Join(ASeparator, LNewValues); +end; + +class procedure TPyEnvironmentOTAHelper.StrResetLength(var S: string); +begin + SetLength(S, StrLen(PChar(S))); +end; + +class function TPyEnvironmentOTAHelper.TryGetProjectOutputPath( + const AProject: IOTAProject; ABuildConfig: IOTABuildConfiguration; + out AOutputPath: string): Boolean; +var + LOptions: IOTAProjectOptions; + LOptionsConfigurations: IOTAProjectOptionsConfigurations; + LRelativeOutputPath: string; +begin + Result := False; + try + if Assigned(AProject) then begin + AOutputPath := TPath.GetDirectoryName(AProject.FileName); + LOptions := AProject.ProjectOptions; + if LOptions <> nil then begin + if not Assigned(ABuildConfig) then begin + LOptionsConfigurations := GetProjectOptionsConfigurations(AProject); + if Assigned(LOptionsConfigurations) then + ABuildConfig := LOptionsConfigurations.ActiveConfiguration; + end; + + if Assigned(ABuildConfig) then begin + LRelativeOutputPath := LOptions.Values[OutputDirPropertyName]; + AOutputPath := ExpandOutputPath(ExpandPath(AOutputPath, LRelativeOutputPath), ABuildConfig); + Result := True; + end else + Result := False; + end else + Result := True; + end; + finally + if not Result then + AOutputPath := ''; + end; +end; + +class function TPyEnvironmentOTAHelper.TryCopyFileToOutputPath( + const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; + const AConfig: TPyEnvironmentProjectConfig; const AFileName: string): Boolean; +var + LProjectOutputPath: string; +begin + Result := False; + if (APlatform <> TPyEnvironmentProjectPlatform.Unknown) and TFile.Exists(AFileName) + and TryGetProjectOutputPath(AProject, APlatform, AConfig, LProjectOutputPath) then + begin + try + if not TDirectory.Exists(LProjectOutputPath) then + TDirectory.CreateDirectory(LProjectOutputPath); + TFile.Copy(AFileName, TPath.Combine(LProjectOutputPath, TPath.GetFileName(AFileName)), True); + Result := True; + except + Result := False; + end; + end; +end; + +class function TPyEnvironmentOTAHelper.TryCopyFileToOutputPathOfActiveBuild( + const AProject: IOTAProject; const AFileName: string): Boolean; +var + LPlatform: TPyEnvironmentProjectPlatform; + LConfig: TPyEnvironmentProjectConfig; +begin + LPlatform := TPyEnvironmentProjectPlatform.Unknown; + LConfig := TPyEnvironmentProjectConfig.Release; + if Assigned(AProject) then begin + LPlatform := TPyEnvironmentProjectPlatform.FromString(AProject.CurrentPlatform); + LConfig := TPyEnvironmentProjectConfig.FromString(AProject.CurrentConfiguration); + end; + Result := TryCopyFileToOutputPath(AProject, LPlatform, LConfig, AFileName); +end; + +class function TPyEnvironmentOTAHelper.TryGetBuildConfig( + const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; + const AConfig: TPyEnvironmentProjectConfig; + out ABuildConfig: IOTABuildConfiguration): Boolean; +var + LOptionsConfigurations: IOTAProjectOptionsConfigurations; + LConfigName: string; + I: Integer; +begin + Result := False; + ABuildConfig := nil; + if APlatform <> TPyEnvironmentProjectPlatform.Unknown then begin + LOptionsConfigurations := GetProjectOptionsConfigurations(AProject); + if Assigned(LOptionsConfigurations) then begin + LConfigName := AConfig.ToString; + for I := LOptionsConfigurations.ConfigurationCount - 1 downto 0 do begin + ABuildConfig := LOptionsConfigurations.Configurations[I]; + if ContainsOptionValue(ABuildConfig.Value[sDefine], LConfigName) then begin + ABuildConfig := ABuildConfig.PlatformConfiguration[APlatform.ToString]; + Exit(Assigned(ABuildConfig)); + end; + end; + end; + end; +end; + +class function TPyEnvironmentOTAHelper.TryGetProjectOutputPath( + const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; + const AConfig: TPyEnvironmentProjectConfig; out AOutputPath: string): Boolean; +var + LBuildConfig: IOTABuildConfiguration; +begin + Result := (APlatform <> TPyEnvironmentProjectPlatform.Unknown) and + TryGetBuildConfig(AProject, APlatform, AConfig, LBuildConfig) and + TryGetProjectOutputPath(AProject, LBuildConfig, AOutputPath) and + TPath.HasValidPathChars(AOutputPath, False); + if not Result then + AOutputPath := ''; +end; + +class function TPyEnvironmentOTAHelper.TryGetProjectOutputPathOfActiveBuild( + const AProject: IOTAProject; out AOutputPath: string): Boolean; +var + LPlatform: TPyEnvironmentProjectPlatform; + LConfig: TPyEnvironmentProjectConfig; +begin + LPlatform := TPyEnvironmentProjectPlatform.Unknown; + LConfig := TPyEnvironmentProjectConfig.Release; + if Assigned(AProject) then begin + LPlatform := TPyEnvironmentProjectPlatform.FromString(AProject.CurrentPlatform); + LConfig := TPyEnvironmentProjectConfig.FromString(AProject.CurrentConfiguration); + end; + Result := TryGetProjectOutputPath(AProject, LPlatform, LConfig, AOutputPath); +end; + +class function TPyEnvironmentOTAHelper.TryRemoveOutputFile( + const AProject: IOTAProject; const APlatform: TPyEnvironmentProjectPlatform; + const AConfig: TPyEnvironmentProjectConfig; AFileName: string): Boolean; +var + LProjectOutputPath: string; +begin + Result := False; + if (APlatform <> TPyEnvironmentProjectPlatform.Unknown) and TPyEnvironmentOTAHelper.TryGetProjectOutputPathOfActiveBuild(AProject, LProjectOutputPath) then begin + AFileName := TPath.Combine(LProjectOutputPath, AFileName); + if TFile.Exists(AFileName) then begin + try + TFile.Delete(AFileName); + Result := True; + except + Result := False; + end; + end; + end; +end; + +class function TPyEnvironmentOTAHelper.TryRemoveOutputFileOfActiveBuild( + const AProject: IOTAProject; const AFileName: string): Boolean; +var + LPlatform: TPyEnvironmentProjectPlatform; + LConfig: TPyEnvironmentProjectConfig; +begin + LPlatform := TPyEnvironmentProjectPlatform.Unknown; + LConfig := TPyEnvironmentProjectConfig.Release; + if Assigned(AProject) then begin + LPlatform := TPyEnvironmentProjectPlatform.FromString(AProject.CurrentPlatform); + LConfig := TPyEnvironmentProjectConfig.FromString(AProject.CurrentConfiguration); + end; + Result := TryRemoveOutputFile(AProject, LPlatform, LConfig, AFileName); +end; + +end. diff --git a/src/Project/IDE/PyEnvironment.Project.IDE.ManagerMenu.pas b/src/Project/IDE/PyEnvironment.Project.IDE.ManagerMenu.pas new file mode 100644 index 0000000..321f7f0 --- /dev/null +++ b/src/Project/IDE/PyEnvironment.Project.IDE.ManagerMenu.pas @@ -0,0 +1,377 @@ +unit PyEnvironment.Project.IDE.ManagerMenu; + +interface + +uses + System.Classes, System.SysUtils, + ToolsAPI, + PyEnvironment.Project.IDE.Deploy, + PyEnvironment.Project.IDE.Types; + +type + TPyEnvironmentProjectManagerMenu = class(TNotifierObject, IOTALocalMenu, IOTAProjectManagerMenu) + strict private + FCaption: string; + FExecuteProc: TProc; + FName: string; + FParent: string; + FPosition: Integer; + FVerb: string; + FChecked: boolean; + strict protected + { IOTALocalMenu } + function GetCaption: string; + function GetChecked: Boolean; virtual; + function GetEnabled: Boolean; virtual; + function GetHelpContext: Integer; + function GetName: string; + function GetParent: string; + function GetPosition: Integer; + function GetVerb: string; + procedure SetCaption(const AValue: string); + procedure SetChecked(AValue: Boolean); + procedure SetEnabled(AValue: Boolean); + procedure SetHelpContext(AValue: Integer); + procedure SetName(const AValue: string); + procedure SetParent(const AValue: string); + procedure SetPosition(AValue: Integer); + procedure SetVerb(const AValue: string); + { IOTAProjectManagerMenu } + function GetIsMultiSelectable: Boolean; + procedure Execute(const AMenuContextList: IInterfaceList); overload; + function PostExecute(const AMenuContextList: IInterfaceList): Boolean; + function PreExecute(const AMenuContextList: IInterfaceList): Boolean; + procedure SetIsMultiSelectable(AValue: Boolean); + public + constructor Create(const ACaption, AVerb: string; const APosition: Integer; + const AExecuteProc: TProc = nil; const AName: string = ''; + const AParent: string = ''; const AChecked: boolean = false); + end; + + TPyEnvironmentProjectManagerMenuSeparator = class(TPyEnvironmentProjectManagerMenu) + public + constructor Create(const APosition: Integer); reintroduce; + end; + + TPyEnvironmentProjectManagerMenuEnablePythonEnvironment = class(TPyEnvironmentProjectManagerMenu) + strict private + FIsPyEnvironmentEnabled: boolean; + strict protected + function GetEnabled: Boolean; override; + public const + MENU_CAPTIONS: array[Boolean] of string = ('Enable Python', 'Disable Python'); + public + constructor Create(const AProject: IOTAProject; const APosition: Integer); reintroduce; + end; + + TPyEnvironmentProjectManagerMenuPythonEnvironmentVersion = class(TPyEnvironmentProjectManagerMenu) + strict private const + MENU_VERB = 'PythonEnvironmentVersion'; + MENU_CAPTION = 'Python Version'; + strict private + FIsPyEnvironmentEnabled: boolean; + strict protected + function GetEnabled: Boolean; override; + procedure AddSubItems(const AProject: IOTAProject; const AList: IInterfaceList); + public + constructor Create(const ASubMenuList: IInterfaceList; const AProject: IOTAProject; const APosition: Integer); reintroduce; + end; + + TPyEnvironmentProjectManagerMenuPythonEnvironmentVersionSubItem = class(TPyEnvironmentProjectManagerMenu) + strict private + FProject: IOTAProject; + FParent: IOTALocalMenu; + FPythonVersion: string; + procedure SetDeployFiles(const AProject: IOTAProject; + const AConfig: TPyEnvironmentProjectConfig; + const APlatform: TPyEnvironmentProjectPlatform; + const AEnabled: Boolean); + procedure SetPythonVersion(const AProject: IOTAProject; + const AEnabled: Boolean); + strict protected + function GetEnabled: Boolean; override; + function GetChecked: boolean; override; + public + constructor Create(const AProject: IOTAProject; const APosition: Integer; + const AParent: IOTALocalMenu; + const APythonVersion: string); reintroduce; + end; + +implementation + +uses + System.IOUtils, + PyEnvironment.Project, + PyEnvironment.Project.IDE.Helper; + +{ TPyEnvironmentProjectManagerMenu } + +constructor TPyEnvironmentProjectManagerMenu.Create(const ACaption, AVerb: string; + const APosition: Integer; const AExecuteProc: TProc = nil; + const AName: string = ''; const AParent: string = ''; const AChecked: boolean = false); +begin + inherited Create; + FCaption := ACaption; + FName := AName; + FParent := AParent; + FPosition := APosition; + FVerb := AVerb; + FExecuteProc := AExecuteProc; + FChecked := AChecked; +end; + +procedure TPyEnvironmentProjectManagerMenu.Execute(const AMenuContextList: IInterfaceList); +begin + if Assigned(FExecuteProc) then + FExecuteProc; +end; + +function TPyEnvironmentProjectManagerMenu.GetCaption: string; +begin + Result := FCaption; +end; + +function TPyEnvironmentProjectManagerMenu.GetChecked: Boolean; +begin + Result := FChecked; +end; + +function TPyEnvironmentProjectManagerMenu.GetEnabled: Boolean; +begin + Result := True; +end; + +function TPyEnvironmentProjectManagerMenu.GetHelpContext: Integer; +begin + Result := 0; +end; + +function TPyEnvironmentProjectManagerMenu.GetIsMultiSelectable: Boolean; +begin + Result := False; +end; + +function TPyEnvironmentProjectManagerMenu.GetName: string; +begin + Result := FName; +end; + +function TPyEnvironmentProjectManagerMenu.GetParent: string; +begin + Result := FParent; +end; + +function TPyEnvironmentProjectManagerMenu.GetPosition: Integer; +begin + Result := FPosition; +end; + +function TPyEnvironmentProjectManagerMenu.GetVerb: string; +begin + Result := FVerb; +end; + +function TPyEnvironmentProjectManagerMenu.PostExecute(const AMenuContextList: IInterfaceList): Boolean; +begin + Result := False; +end; + +function TPyEnvironmentProjectManagerMenu.PreExecute(const AMenuContextList: IInterfaceList): Boolean; +begin + Result := False; +end; + +procedure TPyEnvironmentProjectManagerMenu.SetCaption(const AValue: string); +begin +end; + +procedure TPyEnvironmentProjectManagerMenu.SetChecked(AValue: Boolean); +begin + FChecked := AValue; +end; + +procedure TPyEnvironmentProjectManagerMenu.SetEnabled(AValue: Boolean); +begin +end; + +procedure TPyEnvironmentProjectManagerMenu.SetHelpContext(AValue: Integer); +begin +end; + +procedure TPyEnvironmentProjectManagerMenu.SetIsMultiSelectable(AValue: Boolean); +begin +end; + +procedure TPyEnvironmentProjectManagerMenu.SetName(const AValue: string); +begin + FName := AValue; +end; + +procedure TPyEnvironmentProjectManagerMenu.SetParent(const AValue: string); +begin + FParent := AValue; +end; + +procedure TPyEnvironmentProjectManagerMenu.SetPosition(AValue: Integer); +begin + FPosition := AValue; +end; + +procedure TPyEnvironmentProjectManagerMenu.SetVerb(const AValue: string); +begin +end; + +{ TPyEnvironmentProjectManagerMenuSeparator } + +constructor TPyEnvironmentProjectManagerMenuSeparator.Create(const APosition: Integer); +begin + inherited Create('-', '', APosition); +end; + +{ TPyEnvironmentProjectManagerMenuEnablePythonEnvironment } + +constructor TPyEnvironmentProjectManagerMenuEnablePythonEnvironment.Create( + const AProject: IOTAProject; const APosition: Integer); +begin + FIsPyEnvironmentEnabled := TPyEnvironmentProjectHelper.IsPyEnvironmentDefined[AProject]; + inherited Create(MENU_CAPTIONS[FIsPyEnvironmentEnabled], String.Empty, APosition, + procedure() begin + FIsPyEnvironmentEnabled := not FIsPyEnvironmentEnabled; + if not FIsPyEnvironmentEnabled then begin + TPyEnvironmentProjectHelper.RemoveDeployFilesOfClass(AProject); + TPyEnvironmentProjectHelper.CurrentPythonVersion[AProject] := String.Empty; + end; + TPyEnvironmentProjectHelper.IsPyEnvironmentDefined[AProject] := FIsPyEnvironmentEnabled; + end); +end; + +function TPyEnvironmentProjectManagerMenuEnablePythonEnvironment.GetEnabled: Boolean; +begin + Result := FIsPyEnvironmentEnabled or TPyEnvironmentProjectDeploy.Found; +end; + +{ TPyEnvironmentProjectManagerMenuPythonEnvironmentVersion } + +procedure TPyEnvironmentProjectManagerMenuPythonEnvironmentVersion.AddSubItems( + const AProject: IOTAProject; const AList: IInterfaceList); +var + LPythonVersion: string; + LPosition: Integer; +begin + LPosition := 0; + for LPythonVersion in TPyEnvironmentProjectDeploy.PYTHON_VERSIONS do begin + AList.Add( + TPyEnvironmentProjectManagerMenuPythonEnvironmentVersionSubItem.Create( + AProject, GetPosition() + LPosition, Self, LPythonVersion + )); + Inc(LPosition); + end; +end; + +constructor TPyEnvironmentProjectManagerMenuPythonEnvironmentVersion.Create( + const ASubMenuList: IInterfaceList; const AProject: IOTAProject; + const APosition: Integer); +begin + FIsPyEnvironmentEnabled := TPyEnvironmentProjectHelper.IsPyEnvironmentDefined[AProject]; + inherited Create(MENU_CAPTION, MENU_VERB, APosition); + AddSubItems(AProject, ASubMenuList); +end; + +function TPyEnvironmentProjectManagerMenuPythonEnvironmentVersion.GetEnabled: Boolean; +begin + Result := FIsPyEnvironmentEnabled; +end; + +{ TPyEnvironmentProjectManagerMenuPythonEnvironmentVersionSubItem } + +constructor TPyEnvironmentProjectManagerMenuPythonEnvironmentVersionSubItem.Create( + const AProject: IOTAProject; const APosition: Integer; + const AParent: IOTALocalMenu; const APythonVersion: string); +begin + FProject := AProject; + FParent := AParent; + FPythonVersion := APythonVersion; + inherited Create(APythonVersion, String.Empty, APosition, + procedure() begin + if TPyEnvironmentProjectHelper.CurrentPythonVersion[FProject] <> FPythonVersion then begin + //Remove old version files + SetPythonVersion(FProject, false); + TPyEnvironmentProjectHelper.CurrentPythonVersion[FProject] := FPythonVersion; + //Add new version files + SetPythonVersion(FProject, true); + end; + end, String.Empty, AParent.GetVerb()); +end; + +function TPyEnvironmentProjectManagerMenuPythonEnvironmentVersionSubItem.GetChecked: boolean; +begin + Result := TPyEnvironmentProjectHelper.CurrentPythonVersion[FProject] = FPythonVersion; +end; + +function TPyEnvironmentProjectManagerMenuPythonEnvironmentVersionSubItem.GetEnabled: Boolean; +begin + Result := FParent.GetEnabled(); +end; + +procedure TPyEnvironmentProjectManagerMenuPythonEnvironmentVersionSubItem.SetDeployFiles( + const AProject: IOTAProject; const AConfig: TPyEnvironmentProjectConfig; + const APlatform: TPyEnvironmentProjectPlatform; const AEnabled: Boolean); +var + LDeployFile: TPyEnvironmentDeployFile; + LPythonVersion: string; +begin + if TPyEnvironmentProjectHelper.SupportsEnvironmentDeployment(AProject) then + begin + LPythonVersion := TPyEnvironmentProjectHelper.CurrentPythonVersion[AProject]; + if AEnabled and (APlatform in TPyEnvironmentProjectDeploy.SUPPORTED_PLATFORMS) then begin + for LDeployFile in TPyEnvironmentProjectDeploy.GetDeployFiles(LPythonVersion, APlatform) do + TPyEnvironmentProjectHelper.AddDeployFile(AProject, AConfig, LDeployFile); + end else begin + for LDeployFile in TPyEnvironmentProjectDeploy.GetDeployFiles(LPythonVersion, APlatform) do begin + TPyEnvironmentProjectHelper.RemoveDeployFile( + AProject, AConfig, APlatform, LDeployFile.LocalFileName, LDeployFile.RemotePath); + if LDeployFile.CopyToOutput then + TPyEnvironmentOTAHelper.TryRemoveOutputFile( + AProject, APlatform, AConfig, TPath.GetFileName(LDeployFile.LocalFileName)); + end; + end; + end; +end; + +procedure TPyEnvironmentProjectManagerMenuPythonEnvironmentVersionSubItem.SetPythonVersion( + const AProject: IOTAProject; const AEnabled: Boolean); + + function SupportsPlatform(const APlatform: TPyEnvironmentProjectPlatform): Boolean; + var + LPlatformName: string; + LSupportedPlatform: string; + begin + if APlatform <> TPyEnvironmentProjectPlatform.Unknown then begin + LPlatformName := APlatform.ToString; + for LSupportedPlatform in AProject.SupportedPlatforms do + if SameText(LPlatformName, LSupportedPlatform) then + Exit(True); + end; + Result := False; + end; + +var + LPlatform: TPyEnvironmentProjectPlatform; + LConfig: TPyEnvironmentProjectConfig; + LProjectOptions: IOTAProjectOptions; +begin + for LPlatform := Low(TPyEnvironmentProjectPlatform) to High(TPyEnvironmentProjectPlatform) do + if SupportsPlatform(LPlatform) then + for LConfig := Low(TPyEnvironmentProjectConfig) to High(TPyEnvironmentProjectConfig) do + SetDeployFiles(AProject, LConfig, LPlatform, AEnabled); + + // Remove remaing files from old versions + if not AEnabled then + TPyEnvironmentProjectHelper.RemoveDeployFilesOfClass(AProject); + + LProjectOptions := AProject.ProjectOptions; + if Assigned(LProjectOptions) then + LProjectOptions.ModifiedState := True; +end; + +end. diff --git a/src/Project/IDE/PyEnvironment.Project.IDE.Menu.pas b/src/Project/IDE/PyEnvironment.Project.IDE.Menu.pas new file mode 100644 index 0000000..5a0190d --- /dev/null +++ b/src/Project/IDE/PyEnvironment.Project.IDE.Menu.pas @@ -0,0 +1,206 @@ +unit PyEnvironment.Project.IDE.Menu; + +interface + +uses + System.Classes, + ToolsAPI; + +type + TPyEnvironmentProjectMenuCreatorNotifier = class(TNotifierObject, IOTANotifier, IOTAProjectMenuItemCreatorNotifier) + strict private + class var FNotifierIndex: Integer; + class constructor Create; + class destructor Destroy; + + { IOTAProjectMenuItemCreatorNotifier } + procedure AddMenu(const AProject: IOTAProject; const AIdentList: TStrings; + const AProjectManagerMenuList: IInterfaceList; AIsMultiSelect: Boolean); + public + class procedure Register; static; + end; + + TPyEnvironmenCompileNotifier = class(TInterfacedObject, IOTACompileNotifier) + strict private + const + UnsupportedPlatformMessage = + 'The Python Environment does not support the platform %s in this RAD Studio version.' + sLineBreak + sLineBreak + + 'To avoid problems, disable Python Environment in this project (Project menu > %s) or, if you want to disable it just in ' + + 'a specific platform, set the define directive "%s" in the project settings of this platform. In both cases, ' + + 'be sure you are not using any Python Environment units, otherwise you will get "runtime error" on startup of your application.'; + class var FNotifierIndex: Integer; + class constructor Create; + class destructor Destroy; + { IOTACompileNotifier } + procedure ProjectCompileStarted(const AProject: IOTAProject; AMode: TOTACompileMode); + procedure ProjectCompileFinished(const AProject: IOTAProject; AResult: TOTACompileResult); + procedure ProjectGroupCompileStarted(AMode: TOTACompileMode); + procedure ProjectGroupCompileFinished(AResult: TOTACompileResult); + public + class procedure Register; static; + end; + +implementation + +uses + System.SysUtils, System.IOUtils, + Vcl.Dialogs, + PyEnvironment.Project.IDE.Deploy, + PyEnvironment.Project.IDE.Helper, + PyEnvironment.Project.IDE.Types, + PyEnvironment.Project.IDE.ManagerMenu; + +const + INVALID_MENU_INDEX = -1; + +{ TPyEnvironmentProjectMenuCreatorNotifier } + +class constructor TPyEnvironmentProjectMenuCreatorNotifier.Create; +begin + FNotifierIndex := INVALID_MENU_INDEX; +end; + +class destructor TPyEnvironmentProjectMenuCreatorNotifier.Destroy; +var + LProjectManager: IOTAProjectManager; +begin + if (FNotifierIndex > INVALID_MENU_INDEX) + and Supports(BorlandIDEServices, IOTAProjectManager, LProjectManager) then + LProjectManager.RemoveMenuItemCreatorNotifier(FNotifierIndex); +end; + +class procedure TPyEnvironmentProjectMenuCreatorNotifier.Register; +var + LProjectManager: IOTAProjectManager; +begin + if (FNotifierIndex <= INVALID_MENU_INDEX) + and Supports(BorlandIDEServices, IOTAProjectManager, LProjectManager) then + FNotifierIndex := LProjectManager.AddMenuItemCreatorNotifier( + TPyEnvironmentProjectMenuCreatorNotifier.Create()); +end; + +procedure TPyEnvironmentProjectMenuCreatorNotifier.AddMenu( + const AProject: IOTAProject; const AIdentList: TStrings; + const AProjectManagerMenuList: IInterfaceList; AIsMultiSelect: Boolean); +begin + if (not AIsMultiSelect) + and (AIdentList.IndexOf(sProjectContainer) <> -1) + and Assigned(AProjectManagerMenuList) + and TPyEnvironmentProjectHelper.SupportsEnvironmentDeployment(AProject) then + begin + AProjectManagerMenuList.Add( + TPyEnvironmentProjectManagerMenuSeparator.Create(pmmpRunNoDebug + 10)); + AProjectManagerMenuList.Add( + TPyEnvironmentProjectManagerMenuEnablePythonEnvironment.Create( + AProject, pmmpRunNoDebug + 20)); + AProjectManagerMenuList.Add( + TPyEnvironmentProjectManagerMenuPythonEnvironmentVersion.Create( + AProjectManagerMenuList, AProject, pmmpRunNoDebug + 30)); + end; +end; + +{ TPyEnvironmenCompileNotifier } + +class constructor TPyEnvironmenCompileNotifier.Create; +begin + FNotifierIndex := INVALID_MENU_INDEX; +end; + +class destructor TPyEnvironmenCompileNotifier.Destroy; +var + LCompileServices: IOTACompileServices; +begin + if (FNotifierIndex > INVALID_MENU_INDEX) + and Supports(BorlandIDEServices, IOTACompileServices, LCompileServices) then + LCompileServices.RemoveNotifier(FNotifierIndex); +end; + +procedure TPyEnvironmenCompileNotifier.ProjectCompileFinished( + const AProject: IOTAProject; AResult: TOTACompileResult); +begin + // +end; + +procedure TPyEnvironmenCompileNotifier.ProjectCompileStarted( + const AProject: IOTAProject; AMode: TOTACompileMode); +var + LPlatform: TPyEnvironmentProjectPlatform; + LConfig: TPyEnvironmentProjectConfig; + LDeployFile: TPyEnvironmentDeployFile; + LPythonVersion: string; +begin + if TPyEnvironmentProjectHelper.SupportsEnvironmentDeployment(AProject) then + begin + if Assigned(AProject) then + LPlatform := TPyEnvironmentProjectPlatform.FromString(AProject.CurrentPlatform) + else + LPlatform := TPyEnvironmentProjectPlatform.Unknown; + if LPlatform = TPyEnvironmentProjectPlatform.Unknown then + Exit; + + if (AMode in [TOTACompileMode.cmOTAMake, TOTACompileMode.cmOTABuild]) and + TPyEnvironmentProjectHelper.IsPyEnvironmentDefined[AProject] and TPyEnvironmentProjectDeploy.Found then + begin + LPythonVersion := TPyEnvironmentProjectHelper.CurrentPythonVersion[AProject]; + LConfig := TPyEnvironmentProjectConfig.FromString(AProject.CurrentConfiguration); + if TPyEnvironmentProjectHelper.IsPyEnvironmentDefinedForPlatform(AProject, LPlatform, LConfig) then begin + if LPlatform in TPyEnvironmentProjectDeploy.SUPPORTED_PLATFORMS then begin + TPyEnvironmentProjectHelper.RemoveUnexpectedDeployFilesOfClass( + AProject, LConfig, LPlatform, TPyEnvironmentProjectDeploy.GetDeployFiles(LPythonVersion, LPlatform)); + for LDeployFile in TPyEnvironmentProjectDeploy.GetDeployFiles(LPythonVersion, LPlatform) do begin + if LDeployFile.CopyToOutput then begin + Assert(LDeployFile.LocalFileName <> ''); + TPyEnvironmentOTAHelper.TryCopyFileToOutputPathOfActiveBuild(AProject, + TPath.Combine(TPyEnvironmentProjectDeploy.AbsolutePath, LDeployFile.LocalFileName)); + end; + TPyEnvironmentProjectHelper.AddDeployFile(AProject, LConfig, LDeployFile); + end; + end else begin + for LDeployFile in TPyEnvironmentProjectDeploy.GetDeployFiles(LPythonVersion, LPlatform) do + TPyEnvironmentProjectHelper.RemoveDeployFile( + AProject, LConfig, LPlatform, LDeployFile.LocalFileName, LDeployFile.RemotePath); + TPyEnvironmentProjectHelper.RemoveDeployFilesOfClass(AProject, LConfig, LPlatform); + Showmessage(Format(UnsupportedPlatformMessage, [AProject.CurrentPlatform, + TPyEnvironmentProjectManagerMenuEnablePythonEnvironment.MENU_CAPTIONS[True], + TPyEnvironmentProjectDeploy.PROJECT_NO_USE_PYTHON])); + end; + end else begin + for LDeployFile in TPyEnvironmentProjectDeploy.GetDeployFiles(LPythonVersion, LPlatform) do + TPyEnvironmentProjectHelper.RemoveDeployFile( + AProject, LConfig, LPlatform, LDeployFile.LocalFileName, LDeployFile.RemotePath); + TPyEnvironmentProjectHelper.RemoveDeployFilesOfClass(AProject, LConfig, LPlatform); + end; + end + {$IF CompilerVersion >= 35} + else if (AMode = TOTACompileMode.cmOTAClean) and TPyEnvironmentProjectHelper.IsPyEnvironmentDefined[AProject] then begin + LPythonVersion := TPyEnvironmentProjectHelper.CurrentPythonVersion[AProject]; + for LDeployFile in TPyEnvironmentProjectDeploy.GetDeployFiles(LPythonVersion, LPlatform) do + if LDeployFile.CopyToOutput then + TPyEnvironmentOTAHelper.TryRemoveOutputFileOfActiveBuild(AProject, TPath.GetFileName(LDeployFile.LocalFileName)); + end; + {$ENDIF} + end; +end; + +procedure TPyEnvironmenCompileNotifier.ProjectGroupCompileFinished( + AResult: TOTACompileResult); +begin + +end; + +procedure TPyEnvironmenCompileNotifier.ProjectGroupCompileStarted( + AMode: TOTACompileMode); +begin + +end; + +class procedure TPyEnvironmenCompileNotifier.Register; +var + LCompileServices: IOTACompileServices; +begin + if (FNotifierIndex <= INVALID_MENU_INDEX) + and Supports(BorlandIDEServices, IOTACompileServices, LCompileServices) then + FNotifierIndex := LCompileServices.AddNotifier(TPyEnvironmenCompileNotifier.Create); +end; + +end. diff --git a/src/Project/IDE/PyEnvironment.Project.IDE.Registration.pas b/src/Project/IDE/PyEnvironment.Project.IDE.Registration.pas new file mode 100644 index 0000000..dc5ffa7 --- /dev/null +++ b/src/Project/IDE/PyEnvironment.Project.IDE.Registration.pas @@ -0,0 +1,18 @@ +unit PyEnvironment.Project.IDE.Registration; + +interface + +procedure Register(); + +implementation + +uses + PyEnvironment.Project.IDE.Menu; + +procedure Register(); +begin + TPyEnvironmentProjectMenuCreatorNotifier.Register(); + TPyEnvironmenCompileNotifier.Register(); +end; + +end. diff --git a/src/Project/IDE/PyEnvironment.Project.IDE.Types.pas b/src/Project/IDE/PyEnvironment.Project.IDE.Types.pas new file mode 100644 index 0000000..f528ced --- /dev/null +++ b/src/Project/IDE/PyEnvironment.Project.IDE.Types.pas @@ -0,0 +1,109 @@ +unit PyEnvironment.Project.IDE.Types; + +interface + +uses + DeploymentAPI; + +type + TPyEnvironmentProjectConfig = (Release, Debug); + TPyEnvironmentProjectConfigs = set of TPyEnvironmentProjectConfig; + TPyEnvironmentProjectPlatform = (Unknown, Win32, Win64, Android, Android64, iOSDevice32, iOSDevice64, iOSSimulator, OSX64, OSXARM64, Linux64); + + TPyEvironmentProjectConfigHelper = record helper for TPyEnvironmentProjectConfig + function ToString: string; + class function FromString(const AText: string): TPyEnvironmentProjectConfig; static; + end; + + TPyEvironmentProjectPlatformHelper = record helper for TPyEnvironmentProjectPlatform + function ToString: string; + class function FromString(const AText: string): TPyEnvironmentProjectPlatform; static; + end; + + TPyEnvironmentDeployFile = record + Configs: TPyEnvironmentProjectConfigs; + &Platform: TPyEnvironmentProjectPlatform; + LocalFileName: string; + RemotePath: string; + CopyToOutput: Boolean; + Required: Boolean; + Operation: TDeployOperation; + Condition: string; + + constructor Create(const AConfigs: TPyEnvironmentProjectConfigs; + const APlatform: TPyEnvironmentProjectPlatform; + const ALocalFileName, ARemotePath: string; + const ACopyToOutput, ARequired: boolean; + const AOperation: TDeployOperation; const ACondition: string); overload; + + constructor Create(const APlatform: TPyEnvironmentProjectPlatform; + const ALocalFileName, ARemotePath: string; + const ACopyToOutput, ARequired: boolean; + const AOperation: TDeployOperation; const ACondition: string); overload; + end; + +implementation + +uses + TypInfo; + +{ TPyEnvironmentDeployFile } + +constructor TPyEnvironmentDeployFile.Create( + const APlatform: TPyEnvironmentProjectPlatform; const ALocalFileName, + ARemotePath: string; const ACopyToOutput, ARequired: boolean; + const AOperation: TDeployOperation; const ACondition: string); +begin + Create([Release, Debug], APlatform, ALocalFileName, ARemotePath, + ACopyToOutput, ARequired, AOperation, ACondition); +end; + +constructor TPyEnvironmentDeployFile.Create( + const AConfigs: TPyEnvironmentProjectConfigs; + const APlatform: TPyEnvironmentProjectPlatform; const ALocalFileName, + ARemotePath: string; const ACopyToOutput, ARequired: boolean; + const AOperation: TDeployOperation; const ACondition: string); +begin + Configs := AConfigs; + &Platform := APlatform; + LocalFileName := ALocalFilename; + RemotePath := ARemotePath; + CopyToOutput := ACopyToOutput; + Required := ARequired; + Operation := AOperation; + Condition := ACondition; +end; + +{ TPyEvironmentProjectConfigHelper } + +class function TPyEvironmentProjectConfigHelper.FromString( + const AText: string): TPyEnvironmentProjectConfig; +begin + Result := TPyEnvironmentProjectConfig(GetEnumValue(TypeInfo(TPyEnvironmentProjectConfig), AText)); +end; + +function TPyEvironmentProjectConfigHelper.ToString: string; +begin + Result := GetEnumName(TypeInfo(TPyEnvironmentProjectConfig), Ord(Self)); +end; + +{ TPyEvironmentProjectPlatformHelper } + +class function TPyEvironmentProjectPlatformHelper.FromString( + const AText: string): TPyEnvironmentProjectPlatform; +var + LEnumValue: Integer; +begin + LEnumValue := GetEnumValue(TypeInfo(TPyEnvironmentProjectPlatform), AText); + if LEnumValue = -1 then + Result := TPyEnvironmentProjectPlatform.Unknown + else + Result := TPyEnvironmentProjectPlatform(GetEnumValue(TypeInfo(TPyEnvironmentProjectPlatform), AText)); +end; + +function TPyEvironmentProjectPlatformHelper.ToString: string; +begin + Result := GetEnumName(TypeInfo(TPyEnvironmentProjectPlatform), Ord(Self)); +end; + +end. diff --git a/src/Project/PyEnvironment.Project.pas b/src/Project/PyEnvironment.Project.pas new file mode 100644 index 0000000..f38c93c --- /dev/null +++ b/src/Project/PyEnvironment.Project.pas @@ -0,0 +1,65 @@ +unit PyEnvironment.Project; + +interface + +uses + System.Classes; + +type + TPyEnvironmentProject = class + private + function GetPythonVersion: string; + function GetEnabled: boolean; + private + class constructor Create(); + class destructor Destory(); + public + property Enabled: boolean read GetEnabled; + property PythonVersion: string read GetPythonVersion; + end; + +var + PythonProject: TPyEnvironmentProject; + +implementation + +uses + System.SysUtils, System.Rtti; + +{ TPyEnvironmentProject } + +class constructor TPyEnvironmentProject.Create; +begin + PythonProject := TPyEnvironmentProject.Create(); +end; + +class destructor TPyEnvironmentProject.Destory; +begin + FreeAndNil(PythonProject); +end; + +function TPyEnvironmentProject.GetEnabled: boolean; +begin + {$IFDEF PYTHON} + Result := true; + {$ELSE} + Result := false; + {$ENDIF} +end; + +function TPyEnvironmentProject.GetPythonVersion: string; +begin + {$IFDEF PYTHONVER37} + Result := '3.7'; + {$ELSEIF DEFINED(PYTHONVER38)} + Result := '3.8'; + {$ELSEIF DEFINED(PYTHONVER39)} + Result := '3.9'; + {$ELSEIF DEFINED(PYTHONVER310)} + Result := '3.10'; + {$ELSE} + Result := String.Empty; + {$ENDIF} +end; + +end. diff --git a/src/PyEnvionment.Editors.pas b/src/PyEnvionment.Editors.pas new file mode 100644 index 0000000..227db7c --- /dev/null +++ b/src/PyEnvionment.Editors.pas @@ -0,0 +1,58 @@ +unit PyEnvionment.Editors; + +interface + +uses + System.SysUtils, Classes, DesignIntf, DesignEditors; + +type + TPyEnvironmentEmbeddedPythonVersionProperty = class (TStringProperty) + public + function GetValue: string; override; + procedure SetValue(const Value: string); override; + end; + +procedure Register(); + +implementation + +uses + ToolsAPI, + PyEnvironment.Project.IDE.Helper, + PyEnvironment.Embeddable; + +procedure Register(); +begin + RegisterPropertyEditor(TypeInfo(string), TPyEmbeddedEnvironment, 'PythonVersion', TPyEnvironmentEmbeddedPythonVersionProperty); +end; + +{ TPyEnvironmentEmbeddedPythonVersionProperty } + +function TPyEnvironmentEmbeddedPythonVersionProperty.GetValue: string; +var + LProject: IOTAProject; +begin + LProject := GetActiveProject(); + if Assigned(LProject) then begin + if TPyEnvironmentProjectHelper.IsPyEnvironmentDefined[LProject] then + Result := TPyEnvironmentProjectHelper.CurrentPythonVersion[LProject] + else + Result := inherited; + end else + Result := inherited; +end; + +procedure TPyEnvironmentEmbeddedPythonVersionProperty.SetValue( + const Value: string); +var + LProject: IOTAProject; +begin + LProject := GetActiveProject(); + if Assigned(LProject) then begin + if not TPyEnvironmentProjectHelper.IsPyEnvironmentDefined[LProject] then + inherited; + end else + inherited; +end; + +end. diff --git a/src/PyEnvironment.Path.pas b/src/PyEnvironment.Path.pas index ba6e427..d34f3ef 100644 --- a/src/PyEnvironment.Path.pas +++ b/src/PyEnvironment.Path.pas @@ -34,6 +34,9 @@ interface type TPyEnvironmentPath = class + public const + DEPLOY_PATH = '$(DEPLOY_PATH)'; + public /// /// This function might resolve path variables, relative paths and whatever regarding paths /// @@ -48,16 +51,16 @@ implementation { TPyEnvironmentPath } class function TPyEnvironmentPath.ResolvePath(const APath: string): string; -var - LFilePath: string; begin - if (APath <> ExpandFileName(APath)) then begin + if (APath <> ExpandFileName(APath)) or (APath = DEPLOY_PATH) then begin {$IFDEF ANDROID} - LFilePath := TPath.GetDocumentsPath(); + Result := TPath.GetDocumentsPath(); {$ELSE} - LFilePath := TPath.GetDirectoryName(GetModuleName(HInstance)); + Result := TPath.GetDirectoryName(GetModuleName(HInstance)); {$ENDIF} - Result := TPath.Combine(LFilePath, APath); + + if (APath <> DEPLOY_PATH) then + Result := TPath.Combine(Result, APath); end else Result := APath; end; diff --git a/src/PyEnvironment.pas b/src/PyEnvironment.pas index 5526c59..5a27afb 100644 --- a/src/PyEnvironment.pas +++ b/src/PyEnvironment.pas @@ -96,10 +96,10 @@ TPyCustomEnvironment = class(TComponent, IEnvironmentNotifier