├── .gitignore ├── CHANGES ├── CONTRIBUTING ├── Deploy ├── PupNet.128x128.png ├── PupNet.16x16.png ├── PupNet.24x24.png ├── PupNet.256x256.png ├── PupNet.32x32.png ├── PupNet.48x48.png ├── PupNet.64x64.png ├── PupNet.96x96.png ├── PupNet.ico └── PupNet.metainfo.xml ├── LICENSE ├── Media ├── Devel │ ├── DEV-NOTES.txt │ ├── HelloWorld.source.pdn │ ├── PupNet.1024x1024.pdn │ ├── PupNet.128x128.png │ ├── PupNet.16x16.png │ ├── PupNet.24x24.png │ ├── PupNet.256x256.png │ ├── PupNet.32x32.png │ ├── PupNet.48x48.png │ ├── PupNet.512x512.png │ ├── PupNet.64x64.png │ ├── PupNet.96x96.png │ ├── PupNet.ico │ ├── PupNet.social.pdn │ ├── PupNet.source.pdn │ └── terminal-btns.pdn ├── HelloWorld.1280x388.png ├── Publish-AppImage.png ├── PupNet.1280x388.png ├── PupNet.social.png ├── Screenie-AvantGarde.png ├── Screenie-Configuration.png ├── Screenie-InnoPath.png ├── Screenie-PupNet.png ├── Screenie-Setup.png └── Screenie-StartMenu.png ├── PupNet.Test ├── AppImageBuilderTest.cs ├── AppStreamTest.cs ├── ArgumentParserTest.cs ├── ArgumentReaderTest.cs ├── BuildHostIntegrationTest.cs ├── BuildHostTest.cs ├── ChangeParserTest.cs ├── ConfigurationReaderTest.cs ├── DummyConf.cs ├── IniReaderTest.cs ├── MacroExpanderTest.cs ├── MacroIdTest.cs ├── PackageBuilderTest.cs ├── PupNet.Test.csproj ├── RuntimeConverterTest.cs └── Usings.cs ├── PupNet.pupnet.conf ├── PupNet.sln ├── PupNet ├── ArgumentParser.cs ├── ArgumentReader.cs ├── Assets │ ├── app.128x128.png │ ├── app.16x16.png │ ├── app.24x24.png │ ├── app.256x256.png │ ├── app.32x32.png │ ├── app.48x48.png │ ├── app.64x64.png │ ├── app.96x96.png │ ├── app.ico │ ├── appimagetool-aarch64.AppImage │ ├── appimagetool-armhf.AppImage │ ├── appimagetool-x86_64.AppImage │ ├── generic.128x128.png │ ├── generic.16x16.png │ ├── generic.24x24.png │ ├── generic.256x256.png │ ├── generic.32x32.png │ ├── generic.48x48.png │ ├── generic.64x64.png │ ├── generic.96x96.png │ ├── generic.ico │ ├── generic.svg │ ├── runtime-aarch64 │ ├── runtime-armhf │ ├── runtime-x86_64 │ ├── terminal.128x128.png │ ├── terminal.16x16.png │ ├── terminal.24x24.png │ ├── terminal.256x256.png │ ├── terminal.32x32.png │ ├── terminal.48x48.png │ ├── terminal.64x64.png │ ├── terminal.96x96.png │ └── terminal.ico ├── BuildHost.cs ├── BuilderFactory.cs ├── Builders │ ├── AppImageBuilder.cs │ ├── DebianBuilder.cs │ ├── FlatpakBuilder.cs │ ├── RpmBuilder.cs │ ├── SetupBuilder.cs │ └── ZipBuilder.cs ├── ChangeItem.cs ├── ChangeParser.cs ├── ComfirmPrompt.cs ├── ConfigurationReader.cs ├── DocStyles.cs ├── FileOps.cs ├── IniReader.cs ├── MacroExpander.cs ├── MacroId.cs ├── MetaTemplates.cs ├── PackageBuilder.cs ├── PackageKind.cs ├── Program.cs ├── PupNet.csproj └── RuntimeConverter.cs ├── README.md ├── README.nuget.md ├── TODO └── publish.sh /.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Project Specific 3 | ############################################################################### 4 | 5 | INTERNAL/ 6 | 7 | ############################################################################### 8 | # Hidden 9 | ############################################################################### 10 | 11 | .* 12 | *~ 13 | ~* 14 | 15 | **/.*/ 16 | 17 | *.lock 18 | *.~lock 19 | 20 | Thumbs.db 21 | Thumbs.db:encryptable 22 | desktop.ini 23 | 24 | # Explicitly Allow 25 | !.gitignore 26 | 27 | ############################################################################### 28 | # Ignore Builds & Binaries 29 | ############################################################################### 30 | 31 | # Bin, Obj, Out, Output 32 | **/[Bb][Ii][Nn]/ 33 | **/[Oo][Bb][Jj]/ 34 | **/[Oo][Uu][Tt]/ 35 | **/[Oo][Uu][Tt][Pp][Uu][Tt]/ 36 | 37 | # Build 38 | **/[Bb][Ll][Dd]/ 39 | **/[Bb][Uu][Ii][Ll][Dd]/ 40 | 41 | # Debug, Release, Profile 42 | **/[Dd][Ee][Bb][Uu][Gg]*/ 43 | **/*-[Dd][Ee][Bb][Uu][Gg]/ 44 | **/[Rr][Ee][Ll][Ee][Aa][Ss][Ee]*/ 45 | **/*-[Rr][Ee][Ll][Ee][Aa][Ss][Ee]*/ 46 | **/[Pp][Rr][Oo][Ff][Ii][Ll][Ee]*/ 47 | 48 | # AppDir 49 | **/[Aa][Pp][Pp][Dd][Ii][Rr]*/ 50 | 51 | # Temp 52 | **/[Tt][Mm][Pp]/ 53 | **/[Tt][Ee][Mm][Pp]/ 54 | **/[Ss][Cc][Rr][Aa][Tt][Cc][Hh]/ 55 | 56 | # Logs 57 | **/[Ll][Oo][Gg]/ 58 | **/[Ll][Oo][Gg][Ss]/ 59 | 60 | # Binaries 61 | *.exe 62 | *.dll 63 | 64 | 65 | ############################################################################### 66 | # Object files 67 | ############################################################################### 68 | 69 | *.slo 70 | *.lo 71 | *.o 72 | *.obj 73 | 74 | 75 | ############################################################################### 76 | # Visual Studio 77 | ############################################################################### 78 | 79 | .vs/ 80 | ipch/ 81 | 82 | *.ipch 83 | *.sdf 84 | *.tlog 85 | *.opensdf 86 | *.VC.opendb 87 | *.VC.db 88 | 89 | *.suo 90 | *.user 91 | *.userosscache 92 | *.sln.docstates 93 | *.userosscache 94 | *.sln.docstates 95 | .nuget/ 96 | _ReSharper.* 97 | packages/ 98 | artifacts/ 99 | *.userprefs 100 | *DS_Store 101 | *.sln.ide 102 | 103 | *.aps 104 | *.ncb 105 | *.opendb 106 | *.opensdf 107 | *.sdf 108 | *.cachefile 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | 117 | ############################################################################### 118 | # QT 119 | ############################################################################### 120 | 121 | *.user 122 | *.pro.user* 123 | *.autosave 124 | *.stash 125 | moc_*.cpp 126 | qrc_*.cpp 127 | ui_*.h 128 | 129 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | + VERSION 1.8.0; 2024-03-18 2 | - Built with .NET8 3 | - Added -j,--project command line argument. 4 | - Added SetupGroupName configuration parameter. 5 | - Now detects the $DOTNET_HOST_PATH environment variable and uses it if defined. 6 | - Updated documentation concerning NETSDK1194. #27 7 | - Updated default FlatpakPlatformVersion to latest 23.08 at time of writing. 8 | - Removed dependency on Yaap. 9 | - Bugfix: Added trailing ';' to Categories value of desktop file. #33 10 | - BugFix: Use of ';' between property values did not work. Should now use ',' with pupnet. 11 | 12 | 13 | + VERSION 1.7.1; 2023-10-26 14 | - Bugfix: Fix upgrade-conf bug introduced in 1.7.0. 15 | 16 | 17 | + VERSION 1.7.0; 2023-10-25 18 | - Feature #24: Automatically skips confirmation if environment variable "CI=true" is defined. 19 | - Other minor tweaks. 20 | 21 | 22 | + VERSION 1.6.0; 2023-07-22 23 | - IMPORTANT: RPM now creates output file directly under output directory, rather than under "RPMS/arch" sub-directory. 24 | - Bugfix #21: Inno Setup fails with x86 (dotnet runtime "win-x32") 25 | - Bugfix #23: AppImage desktop file broken (macros ${INSTALL_BIN} and ${INSTALL_EXEC} now have leading forward slash for AppImage) 26 | 27 | 28 | + VERSION 1.5.0; 2023-05-07 29 | - Added ability to parse list items in 'AppDescription' configuration and transpose to HTML. 30 | - Added XML validation of AppStream metadata (will now warn if invalid prior to build). 31 | - Bugfix: The configuration property 'IconFiles' handled icon names of form "name.NxN.png", but failed with "name.N.png". 32 | - The ${PRIME_CATEGORY} macro now defaults to "Utility" if PrimeCategory' configuration is omitted. 33 | - Added extensive new unit testing 34 | - Minor changes to configuration documentation 35 | - Other minor changes 36 | - Ships with: appimagetool 13 (2020-12-31) 37 | - Tested against: rpmbuild RPM version 4.18.1, dpkg 1.21.21, flatpak-builder 1.2.3, InnoSetup 6.2.2 38 | 39 | 40 | + VERSION 1.4.1; 2023-05-06 41 | - Bugfix #16: AppImage and Flatpak builds rejected AppStream metadata if conf.AppDescription was empty. Now defaults to AppShortSummary. 42 | - Bugfix #15: AppImage and Flatpak builds rejected AppStream metadata if conf.AppChangeFileEmpty. 43 | - Bugfix #14: AppImage failed if conf.IconPaths empty. On Linux a default icon will be used (previously worked but was broken due to change). 44 | - Bugfix #13: Incorrect tag in AppStream example text (may be uncommented by user) 45 | - Updated to show CHANGELOG on console only with --verbose command option 46 | - Ships with: appimagetool 13 (2020-12-31) 47 | - Tested against: rpmbuild RPM version 4.18.1, dpkg 1.21.21, flatpak-builder 1.2.3, InnoSetup 6.2.2 48 | 49 | 50 | + VERSION 1.4.0; 2023-05-05 51 | - Added 'AppDescription' property to configuration (IMPORTANT NEW FEATURE - see website for information) 52 | - Added ${APPSTREAM_DESCRIPTION_XML} macro to support release information in AppStream metadata 53 | - Updated AppStream metadata template to include ${APPSTREAM_CHANGELOG_XML} 54 | - Now uses 'AppDescription' content when building both RPM and DEB packages 55 | - Added 'AppChangeFile' property to configuration (IMPORTANT NEW FEATURE - see website for information) 56 | - Added ${APPSTREAM_CHANGELOG_XML} macro to support release information in AppStream metadata 57 | - Updated AppStream metadata template to include ${APPSTREAM_DESCRIPTION_XML} 58 | - The AppChangeFile file is now packaged verbatim to the bin directory of deployments 59 | - Added 'InfoBeforeFile' to Windows Setup in order to show AppChangeFile file content 60 | - Added CHANGELOG section to console output when building a package (contains parsed changes only) 61 | - Added MACRO section (verbose only) to console output when building a package (useful for debugging) 62 | - Extensive updates to README documentation 63 | - Other minor corrections and changes 64 | - Ships with: appimagetool 13 (2020-12-31) 65 | - Tested against: rpmbuild RPM version 4.18.1, dpkg 1.21.21, flatpak-builder 1.2.3, InnoSetup 6.2.2 66 | 67 | 68 | + VERSION 1.3.1; 2023-05-01 69 | - Bugfix: Fix package creation when file path of contents contain spaces (enclose file path with quotes when executing chmod) 70 | 71 | 72 | + VERSION 1.3.0; 2023-04-18 73 | - Added RpmAutoProv and RpmAutoReq properties to configuration 74 | - Added RpmRequires property to configuration to allow specification of RPM dependencies 75 | - Added DebianRecommends property to configuration to allow specification of Debian dependencies 76 | - Now ensures execute permission is set on main executable after dotnet publish (.NET6 does not seem to guarantee this) 77 | - Now ensures that all files after dotnet publish have "other read" permissions (.NET6 does not seem to guarantee this) 78 | - Removed desktop and metainfo validity checks in RPM build as non-essential but problematic where dependencies not met 79 | - Bugfix: The name of the pupnet.conf file was not always displayed when building a package 80 | - Bugfix: Debian Copyright file was not packaged 81 | - Bugfix: May have listed previous build files when building RPM package (display issue only) 82 | - Ships with: appimagetool 13 (2020-12-31) 83 | - Tested against: rpmbuild RPM version 4.18.0, dpkg 1.21.21, flatpak-builder 1.2.3, InnoSetup 6.2.2 84 | 85 | 86 | + VERSION 1.2.2; 2023-04-14 87 | - Now supplied as .NET6 tool (explicitly built against .NET6 LTS rather than .NET7) 88 | - Bugfix: AppImage failed to run on some systems (updated AppImage runtimes) 89 | - Bugfix: Added 'BuildRequires: desktop-file-utils' to RPM spec file 90 | - Ships with: appimagetool 13 (2020-12-31) 91 | - Tested against: rpmbuild RPM version 4.18.0, dpkg 1.21.21, flatpak-builder 1.2.3, InnoSetup 6.2.2 92 | 93 | 94 | + VERSION 1.2.1; 2023-04-13 95 | - Bugfix: Unable to show --help or --version due to recent changes 96 | - Ships with: appimagetool 13 (2020-12-31) 97 | - Tested against: rpmbuild RPM version 4.18.0, dpkg 1.21.21, flatpak-builder 1.2.3, InnoSetup 6.2.2 98 | 99 | 100 | + VERSION 1.2.0; 2023-04-13 101 | - Now ships as a dotnet tool (.NET7) 102 | - Added upgrade-conf CLI option to upgrade existing pupnet.conf file 103 | - Added SetupAdminInstall to configuration to allow install with admin privileges in Windows Setup 104 | - Added SetupSuffixOutput to configuration to allow control of naming of Windows Setup 105 | - Added ${LOCAL_DIRECTORY} macro 106 | - Windows Setup now removes (re-writes) existing program group entries on upgrade 107 | - Windows Setup console window now echos command name and version on launch 108 | - Improved Windows Setup console icon 109 | - Fixed trivial bug in --new command 110 | - Tweaks to formatting of --help output 111 | - Other minor changes 112 | - Ships with: appimagetool 13 (2020-12-31) 113 | - Tested against: rpmbuild RPM version 4.18.0, dpkg 1.21.21, flatpak-builder 1.2.3, InnoSetup 6.2.2 114 | 115 | 116 | + VERSION 1.1.0; 2023-03-30 117 | - Now ships internally with fuse run-times to allow cross-build for: x64, am64 (aarch64) and Arm (32-bit) architectures. 118 | - Support for building AppImages on 32-bit Arm development platforms 119 | - Ships with: appimagetool 13 (2020-12-31) 120 | - Tested against: rpmbuild RPM version 4.18.0, dpkg 1.21.21, flatpak-builder 1.2.3, InnoSetup 6.2.0 121 | 122 | 123 | + VERSION 1.0.1; 2023-03-22 124 | - Now shows AppImageTool version only on Linux. 125 | - Minor updates to built-in help information. 126 | - Added screenshot to metainfo.xml 127 | 128 | 129 | + VERSION 1.0.0; 2023-03-22 130 | - Initial Release 131 | - Tested against: appimagetool: 13 (2020-12-31); rpmbuild: RPM version 4.18.0; dpkg: 1.21.20;flatpak-builder: 1.2.3;InnoSetup: 6.2.0 132 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | ISSUES & IDEAS 2 | 3 | Please do raise issues or share ideas. By raising an issue or sharing an idea, please note that you 4 | give your full permission to the project owner to implement them. 5 | 6 | 7 | PULL REQUESTS 8 | 9 | By submitting a pull request for this project, you agree to assign your copyright of the 10 | contribution to the project owner (merged with or without minor changes). You assert that you have 11 | full power to assign such copyright. 12 | 13 | This would allow us to, for example, change the license of the project or to transfer the ownership 14 | of the project to someone else without your further consent. We require this so that we do not have 15 | to ask everyone who has ever contributed. 16 | 17 | If you feel uncomfortable about this, please do not make a pull request. -------------------------------------------------------------------------------- /Deploy/PupNet.128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.128x128.png -------------------------------------------------------------------------------- /Deploy/PupNet.16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.16x16.png -------------------------------------------------------------------------------- /Deploy/PupNet.24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.24x24.png -------------------------------------------------------------------------------- /Deploy/PupNet.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.256x256.png -------------------------------------------------------------------------------- /Deploy/PupNet.32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.32x32.png -------------------------------------------------------------------------------- /Deploy/PupNet.48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.48x48.png -------------------------------------------------------------------------------- /Deploy/PupNet.64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.64x64.png -------------------------------------------------------------------------------- /Deploy/PupNet.96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.96x96.png -------------------------------------------------------------------------------- /Deploy/PupNet.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Deploy/PupNet.ico -------------------------------------------------------------------------------- /Deploy/PupNet.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MIT 4 | 5 | ${APP_ID} 6 | ${APP_FRIENDLY_NAME} 7 | ${APP_SHORT_SUMMARY} 8 | ${PUBLISHER_NAME} 9 | ${PUBLISHER_LINK_URL} 10 | ${APP_LICENSE_ID} 11 | 12 | 13 | ${APP_ID}.desktop 14 | 15 | 16 | ${APPSTREAM_DESCRIPTION_XML} 17 | 18 | 19 | 20 | 21 | Development 22 | Utility 23 | 24 | 25 | 26 | appimage 27 | flatpak 28 | dotnet 29 | csharp 30 | installation 31 | development 32 | programming 33 | entwicklung 34 | programmierung 35 | windows 36 | linux 37 | setup 38 | debian 39 | rpm 40 | 41 | 42 | 43 | 44 | https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/main/Media/Screenie-PupNet.png 45 | 46 | 47 | 48 | 49 | ${APPSTREAM_CHANGELOG_XML} 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Media/Devel/DEV-NOTES.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DONE 5 | 6 | LICENSE causing RPM to fail. Test others 7 | 8 | Test Deb, RPM 9 | 10 | Hook up license to Inno 11 | Filesafe values 12 | Make XML safe 13 | Check for exec 14 | AssertOK() - update 15 | Need maintainer field 16 | Need PackageName field (lowercase in rpm/deb) 17 | Rigorous checks on name,id, PackageName 18 | Warning for no desktop or appstream 19 | BuildArch in rpm 20 | File to value arg 21 | Fully qualified files 22 | Conf categories 23 | Macro final output 24 | PackRelease 25 | Embed AppImage 26 | Default conf file -- look in directories 27 | Icons 28 | 29 | RPM build warnings: 30 | source_date_epoch_from_changelog set but %changelog is missing 31 | 32 | -------------------------------------------------------------------------------- /Media/Devel/HelloWorld.source.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/HelloWorld.source.pdn -------------------------------------------------------------------------------- /Media/Devel/PupNet.1024x1024.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.1024x1024.pdn -------------------------------------------------------------------------------- /Media/Devel/PupNet.128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.128x128.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.16x16.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.24x24.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.256x256.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.32x32.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.48x48.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.512x512.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.64x64.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.96x96.png -------------------------------------------------------------------------------- /Media/Devel/PupNet.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.ico -------------------------------------------------------------------------------- /Media/Devel/PupNet.social.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.social.pdn -------------------------------------------------------------------------------- /Media/Devel/PupNet.source.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/PupNet.source.pdn -------------------------------------------------------------------------------- /Media/Devel/terminal-btns.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Devel/terminal-btns.pdn -------------------------------------------------------------------------------- /Media/HelloWorld.1280x388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/HelloWorld.1280x388.png -------------------------------------------------------------------------------- /Media/Publish-AppImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Publish-AppImage.png -------------------------------------------------------------------------------- /Media/PupNet.1280x388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/PupNet.1280x388.png -------------------------------------------------------------------------------- /Media/PupNet.social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/PupNet.social.png -------------------------------------------------------------------------------- /Media/Screenie-AvantGarde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Screenie-AvantGarde.png -------------------------------------------------------------------------------- /Media/Screenie-Configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Screenie-Configuration.png -------------------------------------------------------------------------------- /Media/Screenie-InnoPath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Screenie-InnoPath.png -------------------------------------------------------------------------------- /Media/Screenie-PupNet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Screenie-PupNet.png -------------------------------------------------------------------------------- /Media/Screenie-Setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Screenie-Setup.png -------------------------------------------------------------------------------- /Media/Screenie-StartMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/Media/Screenie-StartMenu.png -------------------------------------------------------------------------------- /PupNet.Test/AppImageBuilderTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Runtime.InteropServices; 20 | using KuiperZone.PupNet.Builders; 21 | 22 | namespace KuiperZone.PupNet.Test; 23 | 24 | public class AppImageBuilderTest 25 | { 26 | [Fact] 27 | public void GetRuntimePath_RuntimeExistsForKnownArch() 28 | { 29 | // Ensure files are packaged (even on Windows) 30 | Assert.True(File.Exists(AppImageBuilder.GetRuntimePath(Architecture.X64))); 31 | Assert.True(File.Exists(AppImageBuilder.GetRuntimePath(Architecture.Arm64))); 32 | Assert.True(File.Exists(AppImageBuilder.GetRuntimePath(Architecture.Arm))); 33 | 34 | // There is no RID for linux-x86, so not possible to support for X86 35 | Assert.Throws(() => AppImageBuilder.GetRuntimePath(Architecture.X86)); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /PupNet.Test/AppStreamTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Xml.Linq; 20 | 21 | namespace KuiperZone.PupNet.Test; 22 | 23 | public class AppStreamTest 24 | { 25 | [Fact] 26 | public void MetaTemplate_ExpandsToValidXML() 27 | { 28 | var host = new BuildHost(new DummyConf(PackageKind.AppImage)); 29 | 30 | // Must be escaped 31 | var test = host.Macros.Expand(MetaTemplates.MetaInfo, true); 32 | 33 | // Console.WriteLine("+++++"); 34 | // Console.WriteLine(test); 35 | // Console.WriteLine("+++++"); 36 | 37 | XDocument.Parse(test); 38 | } 39 | 40 | [Fact] 41 | public void MetaTemplate_NoAppDescription_ExpandsToValidXML() 42 | { 43 | var host = new BuildHost(new DummyConf(PackageKind.AppImage, nameof(ConfigurationReader.AppDescription))); 44 | var test = host.Macros.Expand(MetaTemplates.MetaInfo, true); 45 | 46 | XDocument.Parse(test); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /PupNet.Test/ArgumentReaderTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Runtime.InteropServices; 20 | 21 | namespace KuiperZone.PupNet.Test; 22 | 23 | public class ArgumentReaderTest 24 | { 25 | [Fact] 26 | public void Version_DecodeOK() 27 | { 28 | // Also test verbose here 29 | var args = new ArgumentReader("--version --verbose"); 30 | Assert.True(args.ShowVersion); 31 | Assert.True(args.IsVerbose); 32 | } 33 | 34 | [Fact] 35 | public void Help_DecodeOK() 36 | { 37 | var args = new ArgumentReader("-h Macros"); 38 | Assert.Equal("macros", args.ShowHelp); 39 | 40 | args = new ArgumentReader("--help"); 41 | Assert.Equal("true", args.ShowHelp); 42 | } 43 | 44 | [Fact] 45 | public void Value_DecodeOK() 46 | { 47 | // Default 48 | var args = new ArgumentReader("f1.conf"); 49 | Assert.Equal("f1.conf", args.Value); 50 | 51 | args = new ArgumentReader("f2.conf"); 52 | Assert.Equal("f2.conf", args.Value); 53 | } 54 | 55 | [Fact] 56 | public void RuntimeId_DecodeOK() 57 | { 58 | // Default - changes depending on system 59 | var args = new ArgumentReader(); 60 | Assert.Equal(RuntimeConverter.DefaultRuntime, args.Runtime); 61 | 62 | args = new ArgumentReader("-r test1"); 63 | Assert.Equal("test1", args.Runtime); 64 | 65 | args = new ArgumentReader("--runtime test2"); 66 | Assert.Equal("test2", args.Runtime); 67 | } 68 | 69 | [Fact] 70 | public void Kind_DecodeOK() 71 | { 72 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 73 | { 74 | var args = new ArgumentReader(); 75 | Assert.Equal(PackageKind.AppImage, args.Kind); 76 | 77 | args = new ArgumentReader("-k rpm"); 78 | Assert.Equal(PackageKind.Rpm, args.Kind); 79 | } 80 | 81 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 82 | { 83 | var args = new ArgumentReader(); 84 | Assert.Equal(PackageKind.Setup, args.Kind); 85 | 86 | args = new ArgumentReader("-k zip"); 87 | Assert.Equal(PackageKind.Zip, args.Kind); 88 | } 89 | } 90 | 91 | [Fact] 92 | public void VersionRelease_DecodeOK() 93 | { 94 | var args = new ArgumentReader(); 95 | Assert.Null(args.VersionRelease); 96 | 97 | args = new ArgumentReader("-v 5.4.3[2]"); 98 | Assert.Equal("5.4.3[2]", args.VersionRelease); 99 | 100 | args = new ArgumentReader("--app-version 5.4.3[2]"); 101 | Assert.Equal("5.4.3[2]", args.VersionRelease); 102 | } 103 | 104 | [Fact] 105 | public void Property_DecodeOK() 106 | { 107 | var args = new ArgumentReader(); 108 | Assert.Null(args.Property); 109 | 110 | args = new ArgumentReader("-p DEBUG"); 111 | Assert.Equal("DEBUG", args.Property); 112 | 113 | args = new ArgumentReader("--property DEBUG"); 114 | Assert.Equal("DEBUG", args.Property); 115 | } 116 | 117 | [Fact] 118 | public void Output_DecodeOK() 119 | { 120 | var args = new ArgumentReader(); 121 | Assert.Null(args.Output); 122 | 123 | args = new ArgumentReader("-o OutputName"); 124 | Assert.Equal("OutputName", args.Output); 125 | 126 | args = new ArgumentReader("--output OutputName"); 127 | Assert.Equal("OutputName", args.Output); 128 | } 129 | 130 | [Fact] 131 | public void Arch_DecodeOK() 132 | { 133 | var args = new ArgumentReader(); 134 | Assert.Null(args.Arch); 135 | 136 | args = new ArgumentReader("--arch archname"); 137 | Assert.Equal("archname", args.Arch); 138 | } 139 | 140 | [Fact] 141 | public void Run_DecodeOK() 142 | { 143 | var args = new ArgumentReader(); 144 | Assert.False(args.IsRun); 145 | 146 | args = new ArgumentReader("-u"); 147 | Assert.True(args.IsRun); 148 | 149 | args = new ArgumentReader("--run"); 150 | Assert.True(args.IsRun); 151 | } 152 | 153 | [Fact] 154 | public void SkipYes_DecodeOK() 155 | { 156 | var args = new ArgumentReader(); 157 | Assert.False(args.IsSkipYes); 158 | 159 | args = new ArgumentReader("-y"); 160 | Assert.True(args.IsSkipYes); 161 | 162 | args = new ArgumentReader("--skip-yes"); 163 | Assert.True(args.IsSkipYes); 164 | } 165 | 166 | [Fact] 167 | public void Project_DecodeOK() 168 | { 169 | var args = new ArgumentReader(); 170 | Assert.Null(args.Project); 171 | 172 | args = new ArgumentReader("-j path.csproj"); 173 | Assert.Equal("path.csproj", args.Project); 174 | 175 | args = new ArgumentReader("--project path.csproj"); 176 | Assert.Equal("path.csproj", args.Project); 177 | } 178 | 179 | [Fact] 180 | public void New_DecodeOK() 181 | { 182 | var args = new ArgumentReader(); 183 | Assert.Null(args.NewFile); 184 | 185 | args = new ArgumentReader("FileName --new Conf"); 186 | Assert.Equal("conf", args.NewFile); 187 | Assert.Equal("FileName", args.Value); 188 | 189 | args = new ArgumentReader("FileName --new"); 190 | Assert.Equal("true", args.NewFile); 191 | 192 | args = new ArgumentReader("-n Meta"); 193 | Assert.Equal("meta", args.NewFile); 194 | } 195 | 196 | } -------------------------------------------------------------------------------- /PupNet.Test/BuildHostIntegrationTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Runtime.InteropServices; 20 | 21 | namespace KuiperZone.PupNet.Test; 22 | 23 | /// 24 | /// This goes a little beyond the scope of unit test, and performs an "integration test" against . 25 | /// To do this, we must have installed all the third-party builder-tools and we will actually produce a dummy package 26 | /// output for each test. Although not ideal, it is necessary as testing for each package output kind prior to releasing 27 | /// the software is otherwise an intensive and error prone exercise. Here, we run the tests only in RELEASE builds only 28 | /// as each test execution blocks. It is recommended to run the tests with "dotnet test -c Release" ON MOST PLATFORMS 29 | /// prior to releasing a new version of pupnet. NOTE. Cannot be used with Flatpak or Windows Setup (damn!). 30 | /// 31 | public class BuildHostIntegrationTest 32 | { 33 | [Fact] 34 | public void BuildZip_EnsureBuildSucceedsAndOutputExists() 35 | { 36 | // We can always test zip 37 | Assert_BuildPackage(PackageKind.Zip, false); 38 | Assert_BuildPackage(PackageKind.Zip, true); 39 | } 40 | 41 | 42 | #if !DEBUG 43 | [Fact] 44 | public void BuildThirdParty_EnsureBuildSucceedsAndOutputExists() 45 | { 46 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 47 | { 48 | Assert_BuildPackage(PackageKind.AppImage, false); 49 | Assert_BuildPackage(PackageKind.AppImage, true); 50 | 51 | Assert_BuildPackage(PackageKind.Deb, false); 52 | Assert_BuildPackage(PackageKind.Deb, true); 53 | 54 | Assert_BuildPackage(PackageKind.Rpm, false); 55 | Assert_BuildPackage(PackageKind.Rpm, true); 56 | } 57 | } 58 | #endif 59 | 60 | private void Assert_BuildPackage(PackageKind kind, bool complete, bool assertOutput = true) 61 | { 62 | string? metadata = null; 63 | string? manifest = null; 64 | var conf = new TestConfiguration(kind, complete); 65 | 66 | try 67 | { 68 | var host = new BuildHost(conf); 69 | 70 | // Must create build outside of BuildHost.Run() 71 | host.Builder.Create(host.ExpandedDesktop, host.ExpandedMetaInfo); 72 | metadata = host.ExpandedMetaInfo; 73 | manifest = host.Builder.ManifestContent; 74 | 75 | // Regression test - test for correct icon 76 | if (host.Builder.IsLinuxExclusive) 77 | { 78 | // If we do not define icon (i.e complete == false), 79 | // we still get default icon on Linux 80 | Assert.NotNull(host.Builder.PrimaryIcon); 81 | 82 | if (complete) 83 | { 84 | // Icon provided in config below 85 | // Always chooses SVG if one is provided over PNG 86 | Assert.EndsWith("Icon.svg", host.Builder.PrimaryIcon); 87 | } 88 | else 89 | { 90 | // Default icon is built into application. 91 | // We have set DesktopTerminal to true below, so expect "terminal" icon 92 | // We will accept either SVG or max size PNG 93 | bool hasSvg = host.Builder.PrimaryIcon.Contains("terminal.svg");; 94 | bool hasPng = host.Builder.PrimaryIcon.Contains("terminal.256x256"); 95 | Assert.True(hasSvg || hasPng); 96 | } 97 | } 98 | 99 | if (host.Builder.IsOsxExclusive) 100 | { 101 | // Test here 102 | } 103 | 104 | if (host.Builder.IsWindowsExclusive) 105 | { 106 | // Windows? We are not declaring Windows icon currently - must be null 107 | Assert.Null(host.Builder.PrimaryIcon); 108 | } 109 | 110 | 111 | // We do NOT call dotnet publish, and must create dummy app file, otherwise build will fail. 112 | var appPath = Path.Combine(host.Builder.BuildAppBin, host.Builder.AppExecName); 113 | File.WriteAllText(appPath, "Dummy app binary"); 114 | 115 | host.Builder.BuildPackage(); 116 | 117 | if (assertOutput) 118 | { 119 | // Check both file and directory (RPM output a directory) 120 | Assert.True(File.Exists(host.Builder.OutputPath) || Directory.Exists(host.Builder.OutputPath)); 121 | } 122 | } 123 | catch(Exception e) 124 | { 125 | Console.WriteLine("=============================="); 126 | Console.WriteLine($"BUILD FAILED: {kind}, {complete}"); 127 | Console.WriteLine("=============================="); 128 | Console.WriteLine(e); 129 | Console.WriteLine(); 130 | 131 | // For debug if fails 132 | Console.WriteLine("=============================="); 133 | Console.WriteLine($"CONFIGURATION: {kind}, {complete}"); 134 | Console.WriteLine("=============================="); 135 | Console.WriteLine(conf.ToString()); 136 | Console.WriteLine(); 137 | 138 | Console.WriteLine("=============================="); 139 | Console.WriteLine($"METADATA: {kind}, {complete}"); 140 | Console.WriteLine("=============================="); 141 | Console.WriteLine(metadata ?? "[NONE]"); 142 | Console.WriteLine(); 143 | 144 | Console.WriteLine("=============================="); 145 | Console.WriteLine($"MANIFEST: {kind}, {complete}"); 146 | Console.WriteLine("=============================="); 147 | Console.WriteLine(manifest ?? "[NONE]"); 148 | 149 | throw; 150 | } 151 | finally 152 | { 153 | Directory.Delete(conf.OutputDirectory, true); 154 | } 155 | } 156 | 157 | private class TestConfiguration : ConfigurationReader 158 | { 159 | public TestConfiguration(PackageKind kind, bool complete) 160 | : base(new ArgumentReader($"-k {kind} -y --verbose"), Create(complete)) 161 | { 162 | // NB. We need to skip prompt above 163 | } 164 | 165 | private static string[] Create(bool complete) 166 | { 167 | // Use unique temporary directory for everything 168 | var workDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); 169 | Directory.CreateDirectory(workDir); 170 | 171 | var lines = new List(); 172 | 173 | // The idea here is to include only the minimal set of configuration we expect to build successfully 174 | lines.Add($"{nameof(AppBaseName)} = 'HelloWorld'"); 175 | lines.Add($"{nameof(AppFriendlyName)} = Hello World"); 176 | lines.Add($"{nameof(AppId)} = \"net.example.helloworld\""); 177 | lines.Add($"{nameof(AppVersionRelease)} = 5.4.3[2]"); 178 | lines.Add($"{nameof(AppShortSummary)} = Test only"); 179 | lines.Add($"{nameof(AppLicenseId)} = LicenseRef-LICENSE"); 180 | 181 | lines.Add($"{nameof(PublisherName)} = Kuiper Zone"); 182 | lines.Add($"{nameof(PublisherLinkUrl)} = https://kuiper.zone"); 183 | lines.Add($"{nameof(PublisherEmail)} = email@example.net"); 184 | 185 | lines.Add($"{nameof(DesktopTerminal)} = true"); 186 | 187 | lines.Add($"{nameof(OutputDirectory)} = {workDir}"); 188 | 189 | // IMPORTANT - SDK must be installed 190 | lines.Add($"{nameof(FlatpakPlatformRuntime)} = org.freedesktop.Platform"); 191 | lines.Add($"{nameof(FlatpakPlatformSdk)} = org.freedesktop.Sdk"); 192 | lines.Add($"{nameof(FlatpakPlatformVersion)} = \"22.00\""); 193 | lines.Add($"{nameof(SetupMinWindowsVersion)} = 10"); 194 | 195 | // Always include metafile We actually need to create the file here 196 | var metapath = Path.Combine(workDir, "app.metainfo.xml"); 197 | File.WriteAllText(metapath, MetaTemplates.MetaInfo); 198 | lines.Add($"{nameof(MetaFile)} = {metapath}"); 199 | 200 | if (complete) 201 | { 202 | // Here we add extended configuration we consider optional extras 203 | lines.Add($"{nameof(PackageName)} = HelloWorld"); 204 | lines.Add($"{nameof(AppDescription)} = Test description\n\n* bullet1\n- bullet2\nLine2"); 205 | lines.Add($"{nameof(PublisherCopyright)} = Copyright Kuiper Zone"); 206 | lines.Add($"{nameof(PublisherLinkName)} = kuiper.zone"); 207 | 208 | lines.Add($"{nameof(DesktopNoDisplay)} = false"); 209 | lines.Add($"{nameof(PrimeCategory)} = Development"); 210 | lines.Add($"{nameof(AppImageVersionOutput)} = true"); 211 | lines.Add($"{nameof(FlatpakFinishArgs)} = --socket=wayland;--socket=fallback-x11;--filesystem=host;--share=network"); 212 | 213 | lines.Add($"{nameof(RpmAutoReq)} = true"); 214 | lines.Add($"{nameof(RpmAutoProv)} = false"); 215 | 216 | lines.Add($"{nameof(SetupVersionOutput)} = true"); 217 | lines.Add($"{nameof(SetupUninstallScript)} = uninstall.bat"); 218 | lines.Add($"{nameof(SetupCommandPrompt)} = Command Prompt"); 219 | 220 | // Need to create dummy icons in order to get the thing to build (we cheat with dummy files). 221 | // However, we can't write dummy ico for windows because Setup would fail (needs to be valid icon) 222 | var icons = new List(); 223 | icons.Add(Path.Combine(workDir, "Icon.32.png")); 224 | File.WriteAllText(icons[^1], "Dummy file"); 225 | 226 | icons.Add(Path.Combine(workDir, "Icon.64.png")); 227 | File.WriteAllText(icons[^1], "Dummy file"); 228 | 229 | icons.Add(Path.Combine(workDir, "Icon.svg")); 230 | File.WriteAllText(icons[^1], "Dummy file"); 231 | 232 | lines.Add($"{nameof(IconFiles)} = {string.Join(';', icons)}"); 233 | } 234 | 235 | return lines.ToArray(); 236 | } 237 | } 238 | } -------------------------------------------------------------------------------- /PupNet.Test/BuildHostTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet.Test; 20 | 21 | public class BuildHostTest 22 | { 23 | [Fact] 24 | public void Constructor_AppImage_OK() 25 | { 26 | var host = new BuildHost(new DummyConf(PackageKind.AppImage)); 27 | Assert.Equal(PackageKind.AppImage, host.Builder.Kind); 28 | Assert.Equal(2, host.PublishCommands.Count); 29 | } 30 | 31 | [Fact] 32 | public void Constructor_Flatpak_OK() 33 | { 34 | var host = new BuildHost(new DummyConf(PackageKind.Flatpak)); 35 | Assert.Equal(PackageKind.Flatpak, host.Builder.Kind); 36 | Assert.Equal(2, host.PublishCommands.Count); 37 | } 38 | 39 | [Fact] 40 | public void Constructor_Rpm_OK() 41 | { 42 | var host = new BuildHost(new DummyConf(PackageKind.Rpm)); 43 | Assert.Equal(PackageKind.Rpm, host.Builder.Kind); 44 | Assert.Equal(2, host.PublishCommands.Count); 45 | } 46 | 47 | [Fact] 48 | public void Constructor_Debian_OK() 49 | { 50 | var host = new BuildHost(new DummyConf(PackageKind.Deb)); 51 | Assert.Equal(PackageKind.Deb, host.Builder.Kind); 52 | Assert.Equal(2, host.PublishCommands.Count); 53 | } 54 | 55 | [Fact] 56 | public void Constructor_Setup_OK() 57 | { 58 | var host = new BuildHost(new DummyConf(PackageKind.Setup)); 59 | Assert.Equal(PackageKind.Setup, host.Builder.Kind); 60 | Assert.Equal(2, host.PublishCommands.Count); 61 | } 62 | 63 | [Fact] 64 | public void Constructor_Zip_OK() 65 | { 66 | var host = new BuildHost(new DummyConf(PackageKind.Zip)); 67 | Assert.Equal(PackageKind.Zip, host.Builder.Kind); 68 | Assert.Equal(2, host.PublishCommands.Count); 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /PupNet.Test/ChangeParserTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet.Test; 20 | 21 | public class ChangeParserTest 22 | { 23 | private readonly static IReadOnlyList _source; 24 | 25 | static ChangeParserTest() 26 | { 27 | var temp = new List(); 28 | 29 | temp.Add("Test readme"); 30 | temp.Add(""); 31 | temp.Add("+ 1.0.1;Title1;contact@example.com;2023-05-01"); 32 | temp.Add(""); 33 | temp.Add("- Change1-a"); 34 | temp.Add(""); 35 | temp.Add("- Change1-b"); 36 | temp.Add(" second line"); 37 | temp.Add("- Change1-c"); 38 | temp.Add(""); 39 | temp.Add("Some description (ignored)"); 40 | temp.Add("- Other change is ignored"); 41 | temp.Add(""); 42 | temp.Add("+1.0.2 ; Title2 ; 2023-05-02"); 43 | temp.Add("- Change2"); 44 | temp.Add(""); 45 | temp.Add("Some description (ignored)"); 46 | 47 | _source = temp; 48 | } 49 | 50 | [Fact] 51 | public void Constructor_ParsesCorrectly() 52 | { 53 | var changes = new ChangeParser(_source); 54 | 55 | var exp = new List(); 56 | exp.Add(new ChangeItem("1.0.1", new DateTime(2023, 05, 01))); 57 | exp.Add(new ChangeItem("Change1-a")); 58 | exp.Add(new ChangeItem("Change1-b second line")); 59 | exp.Add(new ChangeItem("Change1-c")); 60 | exp.Add(new ChangeItem("1.0.2", new DateTime(2023, 05, 02))); 61 | exp.Add(new ChangeItem("Change2")); 62 | 63 | Assert.Equal(exp, changes.Items); 64 | } 65 | 66 | [Fact] 67 | public void ToString_Text() 68 | { 69 | var changes = new ChangeParser(_source); 70 | 71 | var exp = "+ 1.0.1;2023-05-01\n" + 72 | "- Change1-a\n" + 73 | "- Change1-b second line\n" + 74 | "- Change1-c\n" + 75 | "\n" + 76 | "+ 1.0.2;2023-05-02\n" + 77 | "- Change2"; 78 | 79 | Assert.Equal(exp, changes.ToString()); 80 | } 81 | 82 | [Fact] 83 | public void ToString_Html() 84 | { 85 | var changes = new ChangeParser(_source); 86 | 87 | var exp = "
    \n" + 88 | "
  • Change1-a
  • \n" + 89 | "
  • Change1-b second line
  • \n" + 90 | "
  • Change1-c
  • \n" + 91 | "
\n" + 92 | "\n" + 93 | "
    \n" + 94 | "
  • Change2
  • \n" + 95 | "
"; 96 | 97 | Assert.Equal(exp, changes.ToString(true)); 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /PupNet.Test/DummyConf.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet.Test; 20 | 21 | /// 22 | /// Dummy configuration with test values. The values are expected by unit tests, 23 | /// so changing them will break the tests. 24 | /// 25 | public class DummyConf : ConfigurationReader 26 | { 27 | public const string ExpectSignTool = 28 | "\"C:/Program Files (x86)/Windows Kits/10/bin/10.0.22621.0/x64/signtool.exe\" sign /f \"{#GetEnv('SigningCertificate')}\" /p \"{#GetEnv('SigningCertificatePassword')}\" /tr http://timestamp.sectigo.com /td sha256 /fd sha256 $f"; 29 | 30 | public DummyConf() 31 | : base(new ArgumentReader(), Create()) 32 | { 33 | } 34 | 35 | public DummyConf(ArgumentReader args) 36 | : base(args, Create()) 37 | { 38 | } 39 | 40 | /// 41 | /// Creates demo of given kind. For unit test - if omit is not null, the property name given 42 | /// will be removed from the content, leaving the value to fall back to its default. 43 | /// 44 | public DummyConf(PackageKind kind, string? omit = null) 45 | : base(new ArgumentReader("-k " + kind), Create(omit)) 46 | { 47 | } 48 | 49 | private static string[] Create(string? omit = null) 50 | { 51 | var lines = new List(); 52 | 53 | // Quote variations 54 | lines.Add($"{nameof(AppBaseName)} = 'HelloWorld'"); 55 | lines.Add($"{nameof(AppFriendlyName)} = Hello World"); 56 | lines.Add($"{nameof(AppId)} = \"net.example.helloworld\""); 57 | lines.Add($"{nameof(AppVersionRelease)} = 5.4.3[2]"); 58 | lines.Add($"{nameof(PackageName)} = HelloWorld"); 59 | lines.Add($"{nameof(AppShortSummary)} = Test only"); 60 | lines.Add($"{nameof(AppDescription)} = \n Para1-Line1\n\n\n- Bullet1\n* Bullet2\nPara2-Line1 has ${{MACRO_VAR}}\n"); 61 | lines.Add($"{nameof(AppLicenseId)} = LicenseRef-LICENSE"); 62 | lines.Add($"{nameof(AppLicenseFile)} = LICENSE"); 63 | lines.Add($"{nameof(AppChangeFile)} = CHANGELOG"); 64 | 65 | lines.Add($"{nameof(PublisherName)} = Kuiper Zone"); 66 | lines.Add($"{nameof(PublisherCopyright)} = Copyright Kuiper Zone"); 67 | lines.Add($"{nameof(PublisherLinkName)} = kuiper.zone"); 68 | lines.Add($"{nameof(PublisherLinkUrl)} = https://kuiper.zone"); 69 | lines.Add($"{nameof(PublisherEmail)} = email@example.net"); 70 | 71 | lines.Add($"{nameof(StartCommand)} = helloworld"); 72 | lines.Add($"{nameof(DesktopNoDisplay)} = TRUE"); 73 | lines.Add($"{nameof(DesktopTerminal)} = False"); 74 | lines.Add($"{nameof(PrimeCategory)} = Development"); 75 | lines.Add($"{nameof(DesktopFile)} = app.desktop"); 76 | lines.Add($"{nameof(IconFiles)} = Assets/Icon.32x32.png; Assets/Icon.x48.png; Assets/Icon.64.png; Assets/Icon.ico; Assets/Icon.svg;"); 77 | lines.Add($"{nameof(MetaFile)} = metainfo.xml"); 78 | 79 | lines.Add($"{nameof(DotnetProjectPath)} = HelloProject"); 80 | lines.Add($"{nameof(DotnetPublishArgs)} = --self-contained true"); 81 | lines.Add($"{nameof(DotnetPostPublish)} = PostPublishCommand.sh"); 82 | lines.Add($"{nameof(DotnetPostPublishOnWindows)} = PostPublishCommandOnWindows.bat"); 83 | 84 | lines.Add($"{nameof(OutputDirectory)} = Deploy"); 85 | 86 | lines.Add($"{nameof(AppImageArgs)} = -appargs"); 87 | lines.Add($"{nameof(AppImageVersionOutput)} = true"); 88 | 89 | lines.Add($"{nameof(FlatpakPlatformRuntime)} = org.freedesktop.Platform"); 90 | lines.Add($"{nameof(FlatpakPlatformSdk)} = org.freedesktop.Sdk"); 91 | lines.Add($"{nameof(FlatpakPlatformVersion)} = \"18.00\""); 92 | lines.Add($"{nameof(FlatpakFinishArgs)} = --socket=wayland;--socket=fallback-x11;--filesystem=host;--share=network"); 93 | lines.Add($"{nameof(FlatpakBuilderArgs)} = -flatargs"); 94 | 95 | lines.Add($"{nameof(RpmAutoReq)} = true"); 96 | lines.Add($"{nameof(RpmAutoProv)} = false"); 97 | lines.Add($"{nameof(RpmRequires)} = rpm-requires1;rpm-requires2"); 98 | 99 | lines.Add($"{nameof(DebianRecommends)} = deb-depends1;deb-depends2"); 100 | 101 | lines.Add($"{nameof(SetupAdminInstall)} = true"); 102 | lines.Add($"{nameof(SetupCommandPrompt)} = Command Prompt"); 103 | lines.Add($"{nameof(SetupMinWindowsVersion)} = 6.9"); 104 | lines.Add($"{nameof(SetupSignTool)} = {ExpectSignTool}"); 105 | lines.Add($"{nameof(SetupSuffixOutput)} = Setup"); 106 | lines.Add($"{nameof(SetupVersionOutput)} = true"); 107 | lines.Add($"{nameof(SetupUninstallScript)} = uninstall.bat"); 108 | 109 | Remove(lines, omit); 110 | 111 | return lines.ToArray(); 112 | } 113 | 114 | private static void Remove(List list, string? name) 115 | { 116 | if (!string.IsNullOrEmpty(name)) 117 | { 118 | name += " "; 119 | 120 | for (int n = 0; n < list.Count; ++n) 121 | { 122 | if (list[n].StartsWith(name, StringComparison.OrdinalIgnoreCase)) 123 | { 124 | list.RemoveAt(n); 125 | return; 126 | } 127 | } 128 | } 129 | 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /PupNet.Test/IniReaderTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Text; 20 | 21 | namespace KuiperZone.PupNet.Test; 22 | 23 | public class IniReaderTest 24 | { 25 | [Fact] 26 | public void NoQuoteValue_DecodesOK() 27 | { 28 | Assert.Equal("Hello World", Create().Values["NoQuote"]); 29 | } 30 | 31 | [Fact] 32 | public void SingleQuoteValue_DecodesOK() 33 | { 34 | // No trim inside quote 35 | Assert.Equal(" Hello World ", Create().Values["SingleQuote"]); 36 | } 37 | 38 | [Fact] 39 | public void DoubleQuoteValue_DecodesOK() 40 | { 41 | // No trim inside quote 42 | Assert.Equal(" Hello World ", Create().Values["DoubleQuote"]); 43 | } 44 | 45 | [Fact] 46 | public void MultiLineValue_DecodesOK() 47 | { 48 | // Expect this to trim inside quote 49 | Assert.Equal("Hello World", Create().Values["MultiLine1"]); 50 | 51 | // Concatenates 52 | var sb = new StringBuilder(); 53 | sb.AppendLine("Hello World"); 54 | sb.Append("Hello World"); 55 | Assert.Equal(sb.ToString().ReplaceLineEndings("\n"), Create().Values["MultiLine2"]); 56 | 57 | sb.Clear(); 58 | sb.AppendLine("Hello World"); 59 | sb.AppendLine("Hello World"); 60 | sb.AppendLine("Hello World"); 61 | sb.AppendLine(); 62 | sb.AppendLine("Hello World"); 63 | sb.AppendLine(); 64 | sb.AppendLine(); 65 | sb.Append("Hello World"); 66 | Assert.Equal(sb.ToString().ReplaceLineEndings("\n"), Create().Values["MultiLine3"]); 67 | } 68 | 69 | [Fact] 70 | public void SyntaxError_ThrowsArgumentException() 71 | { 72 | var lines = new List(); 73 | lines.Add("Key = Hello World "); 74 | lines.Add("No equals "); 75 | Assert.Throws(() => new IniReader(lines.ToArray())); 76 | 77 | lines.Clear(); 78 | lines.Add($"MultiLine1 = {IniReader.StartMultiQuote} Hello World"); 79 | lines.Add("No Termination"); 80 | Assert.Throws(() => new IniReader(lines.ToArray())); 81 | } 82 | 83 | private static IniReader Create() 84 | { 85 | var lines = new List(); 86 | 87 | // Quote variations 88 | lines.Add("NoQuote = Hello World "); 89 | lines.Add("SingleQuote = ' Hello World '"); 90 | lines.Add("DoubleQuote = \" Hello World \""); 91 | 92 | lines.Add($" #Comment"); 93 | 94 | lines.Add($"MultiLine1 = {IniReader.StartMultiQuote} Hello World {IniReader.EndMultiQuote}"); 95 | 96 | lines.Add($"MultiLine2 = {IniReader.StartMultiQuote} Hello World"); 97 | lines.Add($"Hello World {IniReader.EndMultiQuote}"); 98 | 99 | lines.Add($"MultiLine3 = {IniReader.StartMultiQuote} Hello World"); 100 | lines.Add($"Hello World"); 101 | lines.Add($"Hello World "); 102 | lines.Add($""); 103 | lines.Add($"Hello World "); 104 | lines.Add($""); 105 | lines.Add($""); 106 | lines.Add($"Hello World "); 107 | lines.Add($" {IniReader.EndMultiQuote}"); 108 | 109 | return new IniReader(lines.ToArray()); 110 | } 111 | 112 | private static void Remove(List list, string? name) 113 | { 114 | if (!string.IsNullOrEmpty(name)) 115 | { 116 | name += " "; 117 | 118 | for (int n = 0; n < list.Count; ++n) 119 | { 120 | if (list[n].StartsWith(name, StringComparison.OrdinalIgnoreCase)) 121 | { 122 | list.RemoveAt(n); 123 | return; 124 | } 125 | } 126 | } 127 | 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /PupNet.Test/MacroExpanderTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Text; 20 | 21 | namespace KuiperZone.PupNet.Test; 22 | 23 | public class MacroExpanderTest 24 | { 25 | [Fact] 26 | public void AppDescriptionToXml_HandlesEmpty() 27 | { 28 | Assert.Empty(MacroExpander.AppDescriptionToXml(Array.Empty())); 29 | Assert.Empty(MacroExpander.AppDescriptionToXml(new string[]{"", " ", ""})); 30 | } 31 | 32 | [Fact] 33 | public void AppDescriptionToXml_HandlesParagraphsAndLists() 34 | { 35 | var value = new List(); 36 | value.Add("Para1 Line1\n"); 37 | value.Add("Para1 Line2\n"); 38 | value.Add("Para1 Line3"); 39 | value.Add(""); 40 | value.Add("Para2 Line1 with escapes <>"); 41 | value.Add(""); 42 | value.Add("Para3 Line1"); 43 | value.Add(""); 44 | value.Add("- List 1-1"); 45 | value.Add("- List 1-2"); 46 | value.Add(""); 47 | value.Add("Para4 Line1"); 48 | value.Add("* List 2-1"); 49 | value.Add("* List 2-2"); 50 | value.Add("Para5 Line1"); 51 | 52 | var exp = new StringBuilder(); 53 | exp.Append("

Para1 Line1\n"); 54 | exp.Append("Para1 Line2\n"); 55 | exp.Append("Para1 Line3

\n"); 56 | exp.Append("\n"); 57 | exp.Append("

Para2 Line1 with escapes <>

\n"); 58 | exp.Append("\n"); 59 | exp.Append("

Para3 Line1

\n"); 60 | exp.Append("\n"); 61 | exp.Append("
    \n"); 62 | exp.Append("
  • List 1-1
  • \n"); 63 | exp.Append("
  • List 1-2
  • \n"); 64 | exp.Append("
\n"); 65 | exp.Append("\n"); 66 | exp.Append("

Para4 Line1

\n"); 67 | exp.Append("\n"); 68 | exp.Append("
    \n"); 69 | exp.Append("
  • List 2-1
  • \n"); 70 | exp.Append("
  • List 2-2
  • \n"); 71 | exp.Append("
\n"); 72 | exp.Append("\n"); 73 | exp.Append("

Para5 Line1

"); 74 | 75 | var html = MacroExpander.AppDescriptionToXml(value); 76 | 77 | // Console.WriteLine("==========================="); 78 | // Console.WriteLine(html); 79 | // Console.WriteLine("==========================="); 80 | 81 | Assert.Equal(exp.ToString(), html); 82 | 83 | // Append trailing list 84 | value.Add(""); 85 | value.Add("+ List 3-1"); 86 | 87 | exp.Append("\n"); 88 | exp.Append("\n"); 89 | exp.Append("
    \n"); 90 | exp.Append("
  • List 3-1
  • \n"); 91 | exp.Append("
"); 92 | 93 | html = MacroExpander.AppDescriptionToXml(value); 94 | Assert.Equal(exp.ToString(), html); 95 | } 96 | 97 | [Fact] 98 | public void Expand_EnsureNoMacroOmitted() 99 | { 100 | // Use factory to create one 101 | var host = new BuildHost(new DummyConf(PackageKind.Zip)); 102 | 103 | var sb = new StringBuilder(); 104 | 105 | foreach (var item in Enum.GetValues()) 106 | { 107 | sb.AppendLine(item.ToVar()); 108 | } 109 | 110 | // Need to remove known embedded macro in test string put there by DummyConf 111 | var test = host.Macros.Expand(sb.ToString()).Replace("${MACRO_VAR}", "MACRO_VAR"); 112 | 113 | // Expect no remaining macros 114 | Console.WriteLine(test); 115 | Assert.DoesNotContain("${", test); 116 | } 117 | 118 | [Fact] 119 | public void Expand_EscapeXMLCharacters() 120 | { 121 | var host = new BuildHost(new DummyConf(PackageKind.Zip)); 122 | 123 | // Has XML chars 124 | var summary = host.Macros.Expand("${APP_SHORT_SUMMARY}", true); 125 | Assert.Equal("Test <application> only", summary); 126 | } 127 | 128 | [Fact] 129 | public void Expand_DoesNotRecurse() 130 | { 131 | var host = new BuildHost(new DummyConf(PackageKind.Zip)); 132 | 133 | // Content we expect ${MACRO_VAR} by DummyConf 134 | var desc = host.Macros.Expand("${APPSTREAM_DESCRIPTION_XML}", true); 135 | Assert.Contains("${MACRO_VAR}", desc); 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /PupNet.Test/MacroIdTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet.Test; 20 | 21 | public class MacroIdTest 22 | { 23 | [Fact] 24 | public void ToName_Regression_AllHaveExpectedNames() 25 | { 26 | // REGRESSION - it would be bad form to change names once the 27 | // software is in the wild, as it break existing configurations 28 | Assert.Equal("APP_BASE_NAME", MacroId.AppBaseName.ToName()); 29 | Assert.Equal("APP_FRIENDLY_NAME", MacroId.AppFriendlyName.ToName()); 30 | Assert.Equal("APP_ID", MacroId.AppId.ToName()); 31 | Assert.Equal("APP_SHORT_SUMMARY", MacroId.AppShortSummary.ToName()); 32 | Assert.Equal("APP_LICENSE_ID", MacroId.AppLicenseId.ToName()); 33 | Assert.Equal("PUBLISHER_NAME", MacroId.PublisherName.ToName()); 34 | Assert.Equal("PUBLISHER_COPYRIGHT", MacroId.PublisherCopyright.ToName()); 35 | Assert.Equal("PUBLISHER_LINK_NAME", MacroId.PublisherLinkName.ToName()); 36 | Assert.Equal("PUBLISHER_LINK_URL", MacroId.PublisherLinkUrl.ToName()); 37 | Assert.Equal("PUBLISHER_EMAIL", MacroId.PublisherEmail.ToName()); 38 | Assert.Equal("DESKTOP_NODISPLAY", MacroId.DesktopNoDisplay.ToName()); 39 | Assert.Equal("DESKTOP_INTEGRATE", MacroId.DesktopIntegrate.ToName()); 40 | Assert.Equal("DESKTOP_TERMINAL", MacroId.DesktopTerminal.ToName()); 41 | Assert.Equal("PRIME_CATEGORY", MacroId.PrimeCategory.ToName()); 42 | 43 | // Others 44 | Assert.Equal("APPSTREAM_DESCRIPTION_XML", MacroId.AppStreamDescriptionXml.ToName()); 45 | Assert.Equal("APPSTREAM_CHANGELOG_XML", MacroId.AppStreamChangelogXml.ToName()); 46 | Assert.Equal("APP_VERSION", MacroId.AppVersion.ToName()); 47 | Assert.Equal("DEPLOY_KIND", MacroId.DeployKind.ToName()); 48 | Assert.Equal("DOTNET_RUNTIME", MacroId.DotnetRuntime.ToName()); 49 | Assert.Equal("BUILD_ARCH", MacroId.BuildArch.ToName()); 50 | Assert.Equal("BUILD_TARGET", MacroId.BuildTarget.ToName()); 51 | Assert.Equal("BUILD_DATE", MacroId.BuildDate.ToName()); 52 | Assert.Equal("BUILD_YEAR", MacroId.BuildYear.ToName()); 53 | Assert.Equal("BUILD_ROOT", MacroId.BuildRoot.ToName()); 54 | Assert.Equal("BUILD_SHARE", MacroId.BuildShare.ToName()); 55 | Assert.Equal("BUILD_APP_BIN", MacroId.BuildAppBin.ToName()); 56 | 57 | // Install locations 58 | Assert.Equal("INSTALL_BIN", MacroId.InstallBin.ToName()); 59 | Assert.Equal("INSTALL_EXEC", MacroId.InstallExec.ToName()); 60 | 61 | // Make sure we've not missed any 62 | foreach (var item in Enum.GetValues()) 63 | { 64 | Console.WriteLine(item); 65 | Assert.NotEmpty(item.ToName()); 66 | } 67 | } 68 | 69 | [Fact] 70 | public void ToHint_AllHaveHints() 71 | { 72 | foreach (var item in Enum.GetValues()) 73 | { 74 | Console.WriteLine(item); 75 | Assert.NotEmpty(item.ToHint()); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /PupNet.Test/PackageBuilderTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using KuiperZone.PupNet.Builders; 20 | 21 | namespace KuiperZone.PupNet.Test; 22 | 23 | public class PackageBuilderTest 24 | { 25 | [Fact] 26 | public void DefaultIcons_Available() 27 | { 28 | Assert.NotEmpty(PackageBuilder.DefaultTerminalIcons); 29 | 30 | foreach (var item in PackageBuilder.DefaultTerminalIcons) 31 | { 32 | Assert.True(File.Exists(item)); 33 | } 34 | 35 | Assert.NotEmpty(PackageBuilder.DefaultGuiIcons); 36 | 37 | foreach (var item in PackageBuilder.DefaultGuiIcons) 38 | { 39 | Assert.True(File.Exists(item)); 40 | } 41 | } 42 | 43 | [Fact] 44 | public void AppImage_DecodesOK() 45 | { 46 | var builder = new AppImageBuilder(new DummyConf()); 47 | 48 | Assert.True(builder.IsLinuxExclusive); 49 | AssertOK(builder, PackageKind.AppImage); 50 | Assert.False(builder.IsWindowsExclusive); 51 | Assert.EndsWith("usr/share/metainfo/net.example.helloworld.appdata.xml", builder.MetaBuildPath); 52 | 53 | // Skip arch - depends on test system -- covered in other tests 54 | Assert.StartsWith("HelloWorld-5.4.3-2.", builder.OutputName); 55 | Assert.EndsWith(".AppImage", builder.OutputName); 56 | } 57 | 58 | [Fact] 59 | public void Flatpak_DecodesOK() 60 | { 61 | var builder = new FlatpakBuilder(new DummyConf()); 62 | 63 | Assert.True(builder.IsLinuxExclusive); 64 | AssertOK(builder, PackageKind.Flatpak); 65 | Assert.False(builder.IsWindowsExclusive); 66 | Assert.EndsWith("usr/share/metainfo/net.example.helloworld.metainfo.xml", builder.MetaBuildPath); 67 | 68 | Assert.StartsWith("HelloWorld-5.4.3-2.", builder.OutputName); 69 | Assert.EndsWith(".flatpak", builder.OutputName); 70 | } 71 | 72 | [Fact] 73 | public void Rpm_DecodesOK() 74 | { 75 | var builder = new RpmBuilder(new DummyConf()); 76 | 77 | Assert.True(builder.IsLinuxExclusive); 78 | AssertOK(builder, PackageKind.Rpm); 79 | Assert.False(builder.IsWindowsExclusive); 80 | Assert.EndsWith("usr/share/metainfo/net.example.helloworld.metainfo.xml", builder.MetaBuildPath); 81 | 82 | Assert.StartsWith("helloworld_5.4.3-2", builder.OutputName); 83 | Assert.EndsWith(".rpm", builder.OutputName); 84 | } 85 | 86 | [Fact] 87 | public void Debian_DecodesOK() 88 | { 89 | var builder = new DebianBuilder(new DummyConf()); 90 | 91 | Assert.True(builder.IsLinuxExclusive); 92 | Assert.False(builder.IsWindowsExclusive); 93 | AssertOK(builder, PackageKind.Deb); 94 | Assert.EndsWith("usr/share/metainfo/net.example.helloworld.metainfo.xml", builder.MetaBuildPath); 95 | 96 | Assert.StartsWith("helloworld_5.4.3-2", builder.OutputName); 97 | Assert.EndsWith(".deb", builder.OutputName); 98 | } 99 | 100 | [Fact] 101 | public void Setup_DecodesOK() 102 | { 103 | var builder = new SetupBuilder(new DummyConf()); 104 | 105 | Assert.False(builder.IsLinuxExclusive); 106 | Assert.True(builder.IsWindowsExclusive); 107 | AssertOK(builder, PackageKind.Setup); 108 | Assert.Null(builder.MetaBuildPath); 109 | 110 | Assert.StartsWith("HelloWorldSetup-5.4.3-2.", builder.OutputName); 111 | Assert.EndsWith(".exe", builder.OutputName); 112 | } 113 | 114 | [Fact] 115 | public void Zip_DecodesOK() 116 | { 117 | var builder = new ZipBuilder(new DummyConf()); 118 | AssertOK(builder, PackageKind.Zip); 119 | Assert.Null(builder.MetaBuildPath); 120 | 121 | Assert.StartsWith("HelloWorld-5.4.3-2.", builder.OutputName); 122 | Assert.EndsWith(".zip", builder.OutputName); 123 | } 124 | 125 | private void AssertOK(PackageBuilder builder, PackageKind kind) 126 | { 127 | Assert.Equal(kind, builder.Kind); 128 | Assert.Equal(kind.TargetsWindows(), !builder.IsLinuxExclusive); 129 | 130 | var appExecName = builder.Runtime.IsWindowsRuntime ? "HelloWorld.exe" : "HelloWorld"; 131 | Assert.Equal(appExecName, builder.AppExecName); 132 | Assert.Equal("5.4.3", builder.AppVersion); 133 | Assert.Equal("2", builder.PackageRelease); 134 | 135 | // Not fully qualified as no assert files 136 | Assert.Equal("Deploy", builder.OutputDirectory); 137 | 138 | if (builder.IsLinuxExclusive) 139 | { 140 | Assert.EndsWith($"usr/bin", builder.BuildUsrBin); 141 | Assert.EndsWith($"usr/share", builder.BuildUsrShare); 142 | Assert.EndsWith($"usr/share/metainfo", builder.BuildShareMeta); 143 | Assert.EndsWith($"usr/share/applications", builder.BuildShareApplications); 144 | Assert.EndsWith($"usr/share/icons", builder.BuildShareIcons); 145 | 146 | Assert.EndsWith($"usr/share/applications/net.example.helloworld.desktop", builder.DesktopBuildPath); 147 | 148 | 149 | Assert.Equal($"Assets/Icon.svg", builder.PrimaryIcon); 150 | 151 | Assert.Contains($"Assets/Icon.svg", builder.IconPaths.Keys); 152 | Assert.Contains($"Assets/Icon.32x32.png", builder.IconPaths.Keys); 153 | Assert.Contains($"Assets/Icon.x48.png", builder.IconPaths.Keys); 154 | Assert.Contains($"Assets/Icon.64.png", builder.IconPaths.Keys); 155 | 156 | // Excluded on windows 157 | Assert.DoesNotContain($"Assets/Icon.ico", builder.IconPaths.Keys); 158 | } 159 | else 160 | if (builder.IsWindowsExclusive) 161 | { 162 | Assert.Null(builder.BuildUsrBin); 163 | Assert.Null(builder.BuildUsrShare); 164 | Assert.Null(builder.BuildShareMeta); 165 | Assert.Null(builder.BuildShareApplications); 166 | Assert.Null(builder.BuildShareIcons); 167 | 168 | Assert.Null(builder.DesktopBuildPath); 169 | 170 | Assert.Equal($"Assets/Icon.ico", builder.PrimaryIcon); 171 | 172 | // These are empty on non-linus, only has PrimaryIcon 173 | Assert.True(builder.IconPaths.Count == 0); 174 | } 175 | else 176 | if (builder.IsOsxExclusive) 177 | { 178 | // Currently unknown 179 | Assert.EndsWith($"usr/bin", builder.BuildUsrBin); 180 | Assert.EndsWith($"usr/share", builder.BuildUsrShare); 181 | } 182 | 183 | Assert.NotEmpty(builder.BuildAppBin); 184 | } 185 | } -------------------------------------------------------------------------------- /PupNet.Test/PupNet.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | false 9 | None 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /PupNet.Test/RuntimeConverterTest.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Runtime.InteropServices; 20 | 21 | namespace KuiperZone.PupNet.Test; 22 | 23 | public class RuntimeConverterTest 24 | { 25 | [Fact] 26 | public void Constructor_Default_MapSystemArch() 27 | { 28 | var r = new RuntimeConverter(null); 29 | Assert.Equal(RuntimeConverter.DefaultRuntime, r.RuntimeId); 30 | } 31 | 32 | [Fact] 33 | public void Constructor_LinuxX64_MapsLinuxX64() 34 | { 35 | // Also test verbose here 36 | var r = new RuntimeConverter("Linux-X64"); 37 | Assert.Equal("linux-x64", r.RuntimeId); 38 | Assert.Equal(Architecture.X64, r.RuntimeArch); 39 | Assert.False(r.IsArchUncertain); 40 | Assert.True(r.IsLinuxRuntime); 41 | Assert.False(r.IsWindowsRuntime); 42 | 43 | r = new RuntimeConverter("linux-musl-x64"); 44 | Assert.Equal("linux-musl-x64", r.RuntimeId); 45 | Assert.Equal(Architecture.X64, r.RuntimeArch); 46 | Assert.True(r.IsLinuxRuntime); 47 | Assert.False(r.IsWindowsRuntime); 48 | 49 | Assert.False(r.IsArchUncertain); 50 | } 51 | 52 | [Fact] 53 | public void Constructor_LinuxArm64_MapsLinuxArm64() 54 | { 55 | var r = new RuntimeConverter("Linux-Arm64"); 56 | Assert.Equal("linux-arm64", r.RuntimeId); 57 | Assert.Equal(Architecture.Arm64, r.RuntimeArch); 58 | Assert.True(r.IsLinuxRuntime); 59 | Assert.False(r.IsWindowsRuntime); 60 | 61 | Assert.False(r.IsArchUncertain); 62 | } 63 | 64 | [Fact] 65 | public void Constructor_LinuxArm_MapsLinuxArm() 66 | { 67 | var r = new RuntimeConverter("Linux-Arm"); 68 | Assert.Equal("linux-arm", r.RuntimeId); 69 | Assert.Equal(Architecture.Arm, r.RuntimeArch); 70 | Assert.True(r.IsLinuxRuntime); 71 | Assert.False(r.IsWindowsRuntime); 72 | 73 | Assert.False(r.IsArchUncertain); 74 | } 75 | 76 | [Fact] 77 | public void Constructor_LinuxX86_MapsLinuxX86() 78 | { 79 | var r = new RuntimeConverter("Linux-X86"); 80 | Assert.Equal("linux-x86", r.RuntimeId); 81 | Assert.Equal(Architecture.X86, r.RuntimeArch); 82 | Assert.True(r.IsLinuxRuntime); 83 | Assert.False(r.IsWindowsRuntime); 84 | 85 | Assert.False(r.IsArchUncertain); 86 | } 87 | 88 | [Fact] 89 | public void Constructor_WinX64_MapsWindowsX64() 90 | { 91 | var r = new RuntimeConverter("Win-X64"); 92 | Assert.Equal("win-x64", r.RuntimeId); 93 | Assert.Equal(Architecture.X64, r.RuntimeArch); 94 | Assert.False(r.IsLinuxRuntime); 95 | Assert.True(r.IsWindowsRuntime); 96 | 97 | Assert.False(r.IsArchUncertain); 98 | } 99 | 100 | [Fact] 101 | public void Constructor_Win10Arm64_MapsWindowsArm64() 102 | { 103 | var r = new RuntimeConverter("Win10-Arm64"); 104 | Assert.Equal("win10-arm64", r.RuntimeId); 105 | Assert.Equal(Architecture.Arm64, r.RuntimeArch); 106 | Assert.False(r.IsLinuxRuntime); 107 | Assert.True(r.IsWindowsRuntime); 108 | 109 | Assert.False(r.IsArchUncertain); 110 | } 111 | 112 | [Fact] 113 | public void Constructor_OsxX64_MapsOsxX64() 114 | { 115 | var r = new RuntimeConverter("OSX-X64"); 116 | Assert.Equal("osx-x64", r.RuntimeId); 117 | Assert.Equal(Architecture.X64, r.RuntimeArch); 118 | Assert.False(r.IsLinuxRuntime); 119 | Assert.False(r.IsWindowsRuntime); 120 | 121 | Assert.False(r.IsArchUncertain); 122 | } 123 | 124 | [Fact] 125 | public void Constructor_Android_MapsLinuxUncertain() 126 | { 127 | var r = new RuntimeConverter("android-arm64"); 128 | Assert.Equal("android-arm64", r.RuntimeId); 129 | Assert.Equal(Architecture.Arm64, r.RuntimeArch); 130 | Assert.False(r.IsLinuxRuntime); 131 | Assert.False(r.IsWindowsRuntime); 132 | 133 | Assert.False(r.IsArchUncertain); 134 | } 135 | 136 | [Fact] 137 | public void Constructor_Tizen_MapsLinuxUncertain() 138 | { 139 | var r = new RuntimeConverter("tizen.7.0.0"); 140 | Assert.Equal("tizen.7.0.0", r.RuntimeId); 141 | Assert.True(r.IsLinuxRuntime); 142 | Assert.False(r.IsWindowsRuntime); 143 | 144 | // Arch unknown 145 | Assert.True(r.IsArchUncertain); 146 | } 147 | 148 | [Fact] 149 | public void ToArchitecture_MapsOK() 150 | { 151 | Assert.Equal(Architecture.X64, RuntimeConverter.ToArchitecture("x64")); 152 | Assert.Equal(Architecture.X64, RuntimeConverter.ToArchitecture("x86_64")); 153 | 154 | Assert.Equal(Architecture.Arm64, RuntimeConverter.ToArchitecture("arm64")); 155 | Assert.Equal(Architecture.Arm64, RuntimeConverter.ToArchitecture("aarch64")); 156 | Assert.Equal(Architecture.Arm64, RuntimeConverter.ToArchitecture("arm_aarch64")); 157 | 158 | Assert.Equal(Architecture.Arm, RuntimeConverter.ToArchitecture("arm")); 159 | Assert.Equal(Architecture.Arm, RuntimeConverter.ToArchitecture("armhf")); 160 | 161 | Assert.Equal(Architecture.X86, RuntimeConverter.ToArchitecture("x86")); 162 | Assert.Equal(Architecture.X86, RuntimeConverter.ToArchitecture("i686")); 163 | 164 | Assert.Throws(() => RuntimeConverter.ToArchitecture("jdue")); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /PupNet.Test/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /PupNet.pupnet.conf: -------------------------------------------------------------------------------- 1 | # PUPNET DEPLOY: 1.8.0 2 | # Use: 'pupnet --help conf' for information. 3 | 4 | # APP PREAMBLE 5 | AppBaseName = PupNet 6 | AppFriendlyName = PupNet Deploy 7 | AppId = zone.kuiper.pupnet 8 | AppVersionRelease = 1.8.0 9 | AppShortSummary = Cross-platform deployment utility which packages your .NET project as a ready-to-ship installation file in a single step. 10 | AppDescription = """ 11 | PupNet Deploy is a cross-platform deployment utility which packages your .NET project as a ready-to-ship 12 | installation file in a single step. 13 | 14 | It has been possible to cross-compile console C# applications for sometime now. More recently, the cross-platform 15 | Avalonia replacement for WPF allows fully-featured GUI applications to target a range of platforms, including: 16 | Linux, Windows, MacOS and Android. 17 | 18 | Now, PupNet Deploy allows you to ship your dotnet application as: 19 | * AppImage for Linux 20 | * Setup File for Windows 21 | * Flatpak 22 | * Debian Binary Package 23 | * RPM Binary Package 24 | * Plain old Zip 25 | 26 | PupNet has good support for internationalization, desktop icons, publisher metadata and custom build operations. 27 | Although developed for .NET, it is also possible to use it to deploy C++ and other kinds of applications. 28 | """ 29 | AppLicenseId = AGPL-3.0-or-later 30 | AppLicenseFile = LICENSE 31 | AppChangeFile = CHANGES 32 | 33 | # PUBLISHER 34 | PublisherName = Kuiper Zone 35 | PublisherCopyright = Copyright (C) Andy Thomas 2024 36 | PublisherLinkName = Project Page 37 | PublisherLinkUrl = https://github.com/kuiperzone/PupNet-Deploy 38 | PublisherEmail = contact@kuiper.zone 39 | 40 | # DESKTOP INTEGRATION 41 | DesktopNoDisplay = true 42 | DesktopTerminal = true 43 | DesktopFile = 44 | StartCommand = pupnet 45 | PrimeCategory = Development 46 | MetaFile = Deploy/PupNet.metainfo.xml 47 | IconFiles = """ 48 | Deploy/PupNet.ico 49 | Deploy/PupNet.16x16.png 50 | Deploy/PupNet.24x24.png 51 | Deploy/PupNet.32x32.png 52 | Deploy/PupNet.48x48.png 53 | Deploy/PupNet.64x64.png 54 | Deploy/PupNet.96x96.png 55 | Deploy/PupNet.128x128.png 56 | Deploy/PupNet.256x256.png 57 | """ 58 | 59 | # DOTNET PUBLISH 60 | DotnetProjectPath = PupNet 61 | DotnetPublishArgs = -p:Version=${APP_VERSION} --self-contained true -p:PublishReadyToRun=true -p:DebugType=None -p:DebugSymbols=false 62 | DotnetPostPublish = 63 | DotnetPostPublishOnWindows = 64 | 65 | # PACKAGE OUTPUT 66 | PackageName = PupNet-Deploy 67 | OutputDirectory = Deploy/OUT 68 | 69 | # APPIMAGE OPTIONS 70 | AppImageArgs = 71 | AppImageVersionOutput = true 72 | 73 | # FLATPAK OPTIONS 74 | FlatpakPlatformRuntime = org.freedesktop.Platform 75 | FlatpakPlatformSdk = org.freedesktop.Sdk 76 | FlatpakPlatformVersion = 23.08 77 | FlatpakFinishArgs = """ 78 | --socket=wayland 79 | --socket=x11 80 | --filesystem=host 81 | --share=network 82 | """ 83 | FlatpakBuilderArgs = 84 | 85 | # RPM OPTIONS 86 | RpmAutoReq = false 87 | RpmAutoProv = true 88 | RpmRequires = """ 89 | krb5-libs 90 | libicu 91 | openssl-libs 92 | zlib 93 | """ 94 | 95 | # DEBIAN OPTIONS 96 | DebianRecommends = """ 97 | libc6 98 | libgcc1 99 | libgcc-s1 100 | libgssapi-krb5-2 101 | libicu 102 | libssl 103 | libstdc++6 104 | libunwind 105 | zlib1g 106 | """ 107 | 108 | # WINDOWS SETUP OPTIONS 109 | SetupGroupName = 110 | SetupAdminInstall = false 111 | SetupCommandPrompt = PupNet Console 112 | SetupMinWindowsVersion = 10 113 | SetupSignTool = 114 | SetupSuffixOutput = 115 | SetupVersionOutput = true -------------------------------------------------------------------------------- /PupNet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PupNet", "PupNet\PupNet.csproj", "{79B244C4-B74E-40D4-906D-30C294A54911}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PupNet.Test", "PupNet.Test\PupNet.Test.csproj", "{F43CAFE1-8212-406C-80A5-13F104F7467B}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".solutionFiles", ".solutionFiles", "{655EB92C-F08C-4992-BC36-28D375C0EE49}" 11 | ProjectSection(SolutionItems) = preProject 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(SolutionProperties) = preSolution 21 | HideSolutionNode = FALSE 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {79B244C4-B74E-40D4-906D-30C294A54911}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {79B244C4-B74E-40D4-906D-30C294A54911}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {79B244C4-B74E-40D4-906D-30C294A54911}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {79B244C4-B74E-40D4-906D-30C294A54911}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {F43CAFE1-8212-406C-80A5-13F104F7467B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {F43CAFE1-8212-406C-80A5-13F104F7467B}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {F43CAFE1-8212-406C-80A5-13F104F7467B}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {F43CAFE1-8212-406C-80A5-13F104F7467B}.Release|Any CPU.Build.0 = Release|Any CPU 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /PupNet/Assets/app.128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.128x128.png -------------------------------------------------------------------------------- /PupNet/Assets/app.16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.16x16.png -------------------------------------------------------------------------------- /PupNet/Assets/app.24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.24x24.png -------------------------------------------------------------------------------- /PupNet/Assets/app.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.256x256.png -------------------------------------------------------------------------------- /PupNet/Assets/app.32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.32x32.png -------------------------------------------------------------------------------- /PupNet/Assets/app.48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.48x48.png -------------------------------------------------------------------------------- /PupNet/Assets/app.64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.64x64.png -------------------------------------------------------------------------------- /PupNet/Assets/app.96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.96x96.png -------------------------------------------------------------------------------- /PupNet/Assets/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/app.ico -------------------------------------------------------------------------------- /PupNet/Assets/appimagetool-aarch64.AppImage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/appimagetool-aarch64.AppImage -------------------------------------------------------------------------------- /PupNet/Assets/appimagetool-armhf.AppImage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/appimagetool-armhf.AppImage -------------------------------------------------------------------------------- /PupNet/Assets/appimagetool-x86_64.AppImage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/appimagetool-x86_64.AppImage -------------------------------------------------------------------------------- /PupNet/Assets/generic.128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.128x128.png -------------------------------------------------------------------------------- /PupNet/Assets/generic.16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.16x16.png -------------------------------------------------------------------------------- /PupNet/Assets/generic.24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.24x24.png -------------------------------------------------------------------------------- /PupNet/Assets/generic.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.256x256.png -------------------------------------------------------------------------------- /PupNet/Assets/generic.32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.32x32.png -------------------------------------------------------------------------------- /PupNet/Assets/generic.48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.48x48.png -------------------------------------------------------------------------------- /PupNet/Assets/generic.64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.64x64.png -------------------------------------------------------------------------------- /PupNet/Assets/generic.96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.96x96.png -------------------------------------------------------------------------------- /PupNet/Assets/generic.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/generic.ico -------------------------------------------------------------------------------- /PupNet/Assets/generic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 24 | 34 | 39 | 44 | 46 | 55 | 60 | 65 | 67 | 76 | 81 | 86 | 88 | 90 | 110 | 115 | 126 | 128 | 133 | 138 | 145 | 152 | 159 | 161 | 163 | 165 | 167 | image/svg+xml 170 | 173 | 176 | 178 | 181 | Openclipart 184 | 186 | 188 | tango applications other 191 | 2010-03-29T09:04:01 194 | "Other applications" icon from <a href="http://tango.freedesktop.org/Tango_Desktop_Project"> Tango Project </a> \n<br><br> \nSince version 0.8.90 Tango Project icons are Public Domain: <a href="http://tango.freedesktop.org/Frequently_Asked_Questions#Terms_of_Use.3F"> Tango Project FAQ </a> 197 | https://openclipart.org/detail/35407/tango-applications-other-by-warszawianka 200 | 202 | 204 | warszawianka 207 | 209 | 211 | 213 | 215 | externalsource 218 | gear 221 | icon 224 | orange 227 | square 230 | tango 233 | 235 | 237 | 239 | 242 | 245 | 248 | 251 | 253 | 255 | 257 | 259 | -------------------------------------------------------------------------------- /PupNet/Assets/runtime-aarch64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/runtime-aarch64 -------------------------------------------------------------------------------- /PupNet/Assets/runtime-armhf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/runtime-armhf -------------------------------------------------------------------------------- /PupNet/Assets/runtime-x86_64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/runtime-x86_64 -------------------------------------------------------------------------------- /PupNet/Assets/terminal.128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.128x128.png -------------------------------------------------------------------------------- /PupNet/Assets/terminal.16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.16x16.png -------------------------------------------------------------------------------- /PupNet/Assets/terminal.24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.24x24.png -------------------------------------------------------------------------------- /PupNet/Assets/terminal.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.256x256.png -------------------------------------------------------------------------------- /PupNet/Assets/terminal.32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.32x32.png -------------------------------------------------------------------------------- /PupNet/Assets/terminal.48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.48x48.png -------------------------------------------------------------------------------- /PupNet/Assets/terminal.64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.64x64.png -------------------------------------------------------------------------------- /PupNet/Assets/terminal.96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.96x96.png -------------------------------------------------------------------------------- /PupNet/Assets/terminal.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuiperzone/PupNet-Deploy/dabed0cc2063c5a2d2c4f780bb6718f4b90cfd16/PupNet/Assets/terminal.ico -------------------------------------------------------------------------------- /PupNet/BuilderFactory.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using KuiperZone.PupNet.Builders; 20 | 21 | namespace KuiperZone.PupNet; 22 | 23 | /// 24 | /// Creates a concrete instance of . 25 | /// 26 | public class BuilderFactory 27 | { 28 | /// 29 | /// Creates. 30 | /// 31 | public PackageBuilder Create(ConfigurationReader conf) 32 | { 33 | switch (conf.Arguments.Kind) 34 | { 35 | case PackageKind.AppImage: return new AppImageBuilder(conf); 36 | case PackageKind.Flatpak: return new FlatpakBuilder(conf); 37 | case PackageKind.Rpm: return new RpmBuilder(conf); 38 | case PackageKind.Deb: return new DebianBuilder(conf); 39 | case PackageKind.Setup: return new SetupBuilder(conf); 40 | case PackageKind.Zip: return new ZipBuilder(conf); 41 | default: throw new ArgumentException($"Invalid or unsupported {nameof(PackageKind)} {conf.Arguments.Kind}"); 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /PupNet/Builders/AppImageBuilder.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Runtime.InteropServices; 20 | 21 | namespace KuiperZone.PupNet.Builders; 22 | 23 | /// 24 | /// Extends for AppImage package. 25 | /// https://docs.appimage.org/reference/appdir.html 26 | /// 27 | public class AppImageBuilder : PackageBuilder 28 | { 29 | /// 30 | /// Constructor. 31 | /// 32 | public AppImageBuilder(ConfigurationReader conf) 33 | : base(conf, PackageKind.AppImage) 34 | { 35 | BuildAppBin = BuildUsrBin 36 | ?? throw new InvalidOperationException(nameof(BuildUsrBin)); 37 | 38 | InstallBin = "/usr/bin"; 39 | 40 | // Not used 41 | ManifestBuildPath = null; 42 | ManifestContent = null; 43 | 44 | var list = new List(); 45 | 46 | if (AppImageTool != null) 47 | { 48 | var arch = Arguments.Arch; 49 | string fuse = arch != null ? GetRuntimePath(RuntimeConverter.ToArchitecture(arch)) : GetRuntimePath(Runtime.RuntimeArch); 50 | var cmd = $"{AppImageTool} {Configuration.AppImageArgs} --runtime-file=\"{fuse}\" \"{BuildRoot}\" \"{OutputPath}\""; 51 | 52 | if (Arguments.IsVerbose) 53 | { 54 | cmd += " --verbose"; 55 | } 56 | 57 | list.Add(cmd); 58 | 59 | if (Arguments.IsRun) 60 | { 61 | list.Add(OutputPath); 62 | } 63 | } 64 | else 65 | { 66 | // Cannot run appimagetool on this platform 67 | // Add as warning, rather than throw as still use this class for unit testing 68 | WarningSink.Add($"CRITICAL. Building of AppImages not supported on {RuntimeConverter.DefaultRuntime} development system"); 69 | } 70 | 71 | PackageCommands = list; 72 | } 73 | 74 | /// 75 | /// Gets the embedded appimagetool version. 76 | /// 77 | public const string AppImageVersion = "Version 13 (2020-12-31)"; 78 | 79 | /// 80 | /// Gets full path to embedded appimagetool. Null if architecture not supported. 81 | /// 82 | public static string? AppImageTool { get; } = GetAppImageTool(); 83 | 84 | /// 85 | /// Implements. 86 | /// 87 | public override string Architecture 88 | { 89 | get 90 | { 91 | if (Arguments.Arch != null) 92 | { 93 | return Arguments.Arch; 94 | } 95 | 96 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.X64) 97 | { 98 | return "x86_64"; 99 | } 100 | 101 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.Arm64) 102 | { 103 | return "aarch64"; 104 | } 105 | 106 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.X86) 107 | { 108 | return "i686"; 109 | } 110 | 111 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.Arm) 112 | { 113 | return "armhf"; 114 | } 115 | 116 | return Runtime.RuntimeArch.ToString().ToLowerInvariant(); 117 | } 118 | } 119 | 120 | /// 121 | /// Implements. 122 | /// 123 | public override string OutputName 124 | { 125 | get { return GetOutputName(Configuration.AppImageVersionOutput, Architecture, ".AppImage"); } 126 | } 127 | 128 | /// 129 | /// Overrides. 130 | /// 131 | public override string? MetaBuildPath 132 | { 133 | get 134 | { 135 | if (BuildShareMeta != null) 136 | { 137 | // Older style name currently required 138 | return $"{BuildShareMeta}/{Configuration.AppId}.appdata.xml"; 139 | } 140 | 141 | return null; 142 | } 143 | } 144 | 145 | /// 146 | /// Implements. 147 | /// 148 | public override string BuildAppBin { get; } 149 | 150 | /// 151 | /// Implements. 152 | /// 153 | public override string InstallBin { get; } 154 | 155 | /// 156 | /// Implements. 157 | /// 158 | public override string? ManifestBuildPath { get; } 159 | 160 | /// 161 | /// Implements. 162 | /// 163 | public override string? ManifestContent { get; } 164 | 165 | /// 166 | /// Implements. 167 | /// 168 | public override IReadOnlyCollection PackageCommands { get; } 169 | 170 | /// 171 | /// Implements. 172 | /// 173 | public override bool SupportsStartCommand { get; } = false; 174 | 175 | /// 176 | /// Implements. 177 | /// 178 | public override bool SupportsPostRun { get; } = true; 179 | 180 | /// 181 | /// Gets path to embedded fuse2 runtime, or throws. 182 | /// 183 | /// 184 | public static string GetRuntimePath(Architecture arch) 185 | { 186 | // From: https://github.com/AppImage/AppImageKit/releases/tag/13 187 | if (arch == System.Runtime.InteropServices.Architecture.X64) 188 | { 189 | return Path.Combine(AssemblyDirectory, "runtime-x86_64"); 190 | } 191 | 192 | if (arch == System.Runtime.InteropServices.Architecture.Arm64) 193 | { 194 | return Path.Combine(AssemblyDirectory, "runtime-aarch64"); 195 | } 196 | 197 | if (arch == System.Runtime.InteropServices.Architecture.Arm) 198 | { 199 | return Path.Combine(AssemblyDirectory, "runtime-armhf"); 200 | } 201 | 202 | throw new ArgumentException($"Unsupported runtime architecture {arch} - must be one of: x64, arm64, arm"); 203 | } 204 | 205 | 206 | /// 207 | /// Overrides and extends. 208 | /// 209 | public override void Create(string? desktop, string? metainfo) 210 | { 211 | base.Create(desktop, metainfo); 212 | 213 | // We need a bodge fix to get AppImage to pass validation. We need desktop and meta in 214 | // two places, one place for AppImage builder itself, and the other to get the meta to 215 | // pass validation. See: https://github.com/AppImage/AppImageKit/issues/603#issuecomment-355105387 216 | Operations.WriteFile(Path.Combine(BuildRoot, Configuration.AppId + ".desktop"), desktop); 217 | Operations.WriteFile(Path.Combine(BuildRoot, Configuration.AppId + ".appdata.xml"), metainfo); 218 | 219 | if (PrimaryIcon != null) 220 | { 221 | Operations.CopyFile(PrimaryIcon, Path.Combine(BuildRoot, Configuration.AppId + Path.GetExtension(PrimaryIcon))); 222 | } 223 | else 224 | { 225 | // Icon expected on Linux. Defaults to be provided in none in conf 226 | throw new InvalidOperationException($"Expected {nameof(PrimaryIcon)} but was null"); 227 | } 228 | 229 | // IMPORTANT - Create AppRun link 230 | // ln -s {target} {link} 231 | Operations.Execute($"ln -s \"{InstallExec.TrimStart('/')}\" \"{Path.Combine(BuildRoot, "AppRun")}\""); 232 | } 233 | 234 | /// 235 | /// Overrides and extends. 236 | /// 237 | public override void BuildPackage() 238 | { 239 | if (AppImageTool == null) 240 | { 241 | throw new InvalidOperationException($"Building of AppImages not supported on {RuntimeConverter.DefaultRuntime} development system"); 242 | } 243 | 244 | var arch = Architecture; 245 | 246 | if (Arguments.Arch == null) 247 | { 248 | if (arch == "aarch64" || arch == "arm64") 249 | { 250 | // Strange convention? More confusion? 251 | // https://discourse.appimage.org/t/how-to-package-for-aarch64/2088 252 | arch = "arm_aarch64"; 253 | } 254 | else 255 | if (arch == "armhf") 256 | { 257 | arch = "arm"; 258 | } 259 | } 260 | 261 | Environment.SetEnvironmentVariable("ARCH", arch); 262 | base.BuildPackage(); 263 | } 264 | 265 | private static string? GetAppImageTool() 266 | { 267 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 268 | { 269 | if (RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.X64) 270 | { 271 | return Path.Combine(AssemblyDirectory, "appimagetool-x86_64.AppImage"); 272 | } 273 | 274 | if (RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64) 275 | { 276 | return Path.Combine(AssemblyDirectory, "appimagetool-aarch64.AppImage"); 277 | } 278 | 279 | if (RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm) 280 | { 281 | return Path.Combine(AssemblyDirectory, "appimagetool-armhf.AppImage"); 282 | } 283 | } 284 | 285 | // Not supported 286 | return null; 287 | } 288 | 289 | } 290 | 291 | -------------------------------------------------------------------------------- /PupNet/Builders/DebianBuilder.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Text; 20 | 21 | namespace KuiperZone.PupNet.Builders; 22 | 23 | /// 24 | /// Extends for Debian package. 25 | /// https://www.baeldung.com/linux/create-debian-package 26 | /// 27 | public sealed class DebianBuilder : PackageBuilder 28 | { 29 | private readonly string _debianPackageName; 30 | 31 | /// 32 | /// Constructor. 33 | /// 34 | public DebianBuilder(ConfigurationReader conf) 35 | : base(conf, PackageKind.Deb) 36 | { 37 | _debianPackageName = Configuration.PackageName.ToLowerInvariant(); 38 | 39 | BuildAppBin = Path.Combine(BuildRoot, "opt", Configuration.AppId); 40 | InstallBin = $"/opt/{Configuration.AppId}"; 41 | 42 | // We do not set the content here 43 | ManifestBuildPath = Path.Combine(BuildRoot, "DEBIAN/control"); 44 | 45 | var list = new List(); 46 | var cmd = "dpkg-deb --root-owner-group "; 47 | 48 | if (Arguments.IsVerbose) 49 | { 50 | cmd += "--verbose "; 51 | } 52 | 53 | var archiveDirectory = Path.Combine(OutputDirectory, OutputName); 54 | cmd += $"--build \"{BuildRoot}\" \"{archiveDirectory}\""; 55 | list.Add(cmd); 56 | PackageCommands = list; 57 | } 58 | 59 | /// 60 | /// Implements. 61 | /// 62 | public override string OutputName 63 | { 64 | get 65 | { 66 | var output = Path.GetFileName(Configuration.Arguments.Output); 67 | 68 | if (string.IsNullOrEmpty(output)) 69 | { 70 | // packagename_version-release_architecture.deb 71 | // https://kerneltalks.com/tools/understanding-package-naming-convention-rpm-deb/ 72 | return $"{_debianPackageName}_{AppVersion}-{PackageRelease}_{Architecture}.deb"; 73 | } 74 | 75 | return output; 76 | } 77 | } 78 | 79 | /// 80 | /// Implements. 81 | /// 82 | public override string Architecture 83 | { 84 | get 85 | { 86 | if (Arguments.Arch != null) 87 | { 88 | return Arguments.Arch; 89 | } 90 | 91 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.X64) 92 | { 93 | return "amd64"; 94 | } 95 | 96 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.Arm64) 97 | { 98 | return "arm64"; 99 | } 100 | 101 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.X86) 102 | { 103 | // Not sure about this? 104 | // https://en.wikipedia.org/wiki/X32_ABI 105 | return "x32"; 106 | } 107 | 108 | return Runtime.RuntimeArch.ToString().ToLowerInvariant(); 109 | } 110 | } 111 | 112 | /// 113 | /// Implements. 114 | /// 115 | public override string BuildAppBin { get; } 116 | 117 | /// 118 | /// Implements. 119 | /// 120 | public override string InstallBin { get; } 121 | 122 | /// 123 | /// Implements. 124 | /// 125 | public override string? ManifestBuildPath { get; } 126 | 127 | /// 128 | /// Implements. 129 | /// 130 | public override string? ManifestContent 131 | { 132 | get { return GetControlFile(); } 133 | } 134 | 135 | /// 136 | /// Implements. 137 | /// 138 | public override IReadOnlyCollection PackageCommands { get; } 139 | 140 | /// 141 | /// Implements. 142 | /// 143 | public override bool SupportsStartCommand { get; } = true; 144 | 145 | /// 146 | /// Implements. 147 | /// 148 | public override bool SupportsPostRun { get; } 149 | 150 | /// 151 | /// Overrides and extends. 152 | /// 153 | public override void Create(string? desktop, string? metainfo) 154 | { 155 | base.Create(desktop, metainfo); 156 | 157 | // Rpm and Deb etc only. These get installed to /opt, but put 'link file' in /usr/bin 158 | if (BuildUsrBin != null && !string.IsNullOrEmpty(Configuration.StartCommand)) 159 | { 160 | // We put app under /opt, so put script link under usr/bin 161 | var path = Path.Combine(BuildUsrBin, Configuration.StartCommand); 162 | var script = $"#!/bin/sh\nexec {InstallExec} \"$@\""; 163 | 164 | if (!File.Exists(path)) 165 | { 166 | Operations.WriteFile(path, script); 167 | Operations.Execute($"chmod a+rx \"{path}\""); 168 | } 169 | } 170 | } 171 | 172 | /// 173 | /// Overrides and extends. 174 | /// 175 | public override void BuildPackage() 176 | { 177 | if (Configuration.AppLicenseFile != null && BuildUsrShare != null) 178 | { 179 | var dest = Path.Combine(BuildUsrShare, "doc", _debianPackageName, "Copyright"); 180 | Operations.CopyFile(Configuration.AppLicenseFile, dest, true); 181 | } 182 | 183 | base.BuildPackage(); 184 | } 185 | 186 | private static string ToSection(string? category) 187 | { 188 | // https://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections 189 | switch (category?.ToLowerInvariant()) 190 | { 191 | case "audiovideo" : return "video"; 192 | case "audio" : return "sound"; 193 | case "video" : return "video"; 194 | case "development" : return "development"; 195 | case "education" : return "education"; 196 | case "game" : return "games"; 197 | case "graphics" : return "graphics"; 198 | case "network" : return "net"; 199 | case "office" : return "text"; 200 | case "science" : return "science"; 201 | case "settings" : return "utils"; 202 | case "system" : return "utils"; 203 | case "utility" : return "utils"; 204 | default: return "misc"; 205 | } 206 | } 207 | 208 | private string GetControlFile() 209 | { 210 | // https://www.debian.org/doc/debian-policy/ch-controlfields.html 211 | var sb = new StringBuilder(); 212 | 213 | sb.AppendLine($"Package: {_debianPackageName}"); 214 | sb.AppendLine($"Version: {AppVersion}-{PackageRelease}"); 215 | 216 | // Section is recommended 217 | // https://askubuntu.com/questions/27513/what-is-the-difference-between-debian-contrib-non-free-and-how-do-they-corresp 218 | // https://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections 219 | sb.AppendLine($"Section: multiverse/{ToSection(Configuration.PrimeCategory)}"); 220 | 221 | sb.AppendLine($"Priority: optional"); 222 | sb.AppendLine($"Architecture: {Architecture}"); 223 | sb.AppendLine($"Description: {Configuration.AppShortSummary}"); 224 | 225 | // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-description 226 | foreach (var item in Configuration.AppDescription) 227 | { 228 | if (!string.IsNullOrEmpty(item)) 229 | { 230 | sb.Append(" "); 231 | sb.AppendLine(item); 232 | } 233 | else 234 | { 235 | sb.AppendLine(" ."); 236 | } 237 | } 238 | 239 | 240 | if (!string.IsNullOrEmpty(Configuration.PublisherLinkUrl)) 241 | { 242 | sb.AppendLine($"Homepage: {Configuration.PublisherLinkUrl}"); 243 | } 244 | 245 | // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-maintainer 246 | sb.AppendLine($"Maintainer: {Configuration.PublisherEmail}"); 247 | 248 | // Treated as comments 249 | sb.AppendLine($"License: {Configuration.AppLicenseId}"); 250 | sb.AppendLine($"Vendor: {Configuration.PublisherName}"); 251 | 252 | bool started = false; 253 | foreach (var item in Configuration.DebianRecommends) 254 | { 255 | sb.Append(started ? ", " : "Recommends: "); 256 | sb.Append(item); 257 | started = true; 258 | } 259 | 260 | // Required 261 | sb.AppendLine(); 262 | 263 | return sb.ToString(); 264 | } 265 | 266 | } 267 | 268 | -------------------------------------------------------------------------------- /PupNet/Builders/FlatpakBuilder.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Text; 20 | 21 | namespace KuiperZone.PupNet.Builders; 22 | 23 | /// 24 | /// Extends for Flatpak package. 25 | /// 26 | public class FlatpakBuilder : PackageBuilder 27 | { 28 | /// 29 | /// Constructor. 30 | /// 31 | public FlatpakBuilder(ConfigurationReader conf) 32 | : base(conf, PackageKind.Flatpak) 33 | { 34 | BuildAppBin = BuildUsrBin ?? throw new ArgumentNullException(nameof(BuildUsrBin)); 35 | 36 | // Not used 37 | InstallBin = ""; 38 | 39 | ManifestContent = GetFlatpakManifest(); 40 | ManifestBuildPath = Path.Combine(Root, Configuration.AppBaseName + ".yml"); 41 | 42 | var temp = Path.Combine(Root, "build"); 43 | var state = Path.Combine(Root, "state"); 44 | var repo = Path.Combine(Root, "repo"); 45 | 46 | var cmd = $"flatpak-builder {Configuration.FlatpakBuilderArgs}"; 47 | 48 | if (Arguments.Arch != null) 49 | { 50 | // Explicit only (otherwise leave it to utility to determine) 51 | cmd += $" --arch ${Arguments.Arch}"; 52 | } 53 | 54 | cmd += $" --repo=\"{repo}\" --force-clean \"{temp}\" --state-dir \"{state}\" \"{ManifestBuildPath}\""; 55 | 56 | var list = new List(); 57 | 58 | list.Add(cmd); 59 | list.Add($"flatpak build-bundle \"{repo}\" \"{OutputPath}\" {Configuration.AppId}"); 60 | 61 | if (Arguments.IsRun) 62 | { 63 | list.Add($"flatpak-builder --run \"{temp}\" \"{ManifestBuildPath}\" ${Configuration.AppId} --state-dir \"{state}\""); 64 | } 65 | 66 | PackageCommands = list; 67 | } 68 | 69 | /// 70 | /// Implements. 71 | /// 72 | public override string Architecture 73 | { 74 | get 75 | { 76 | if (Arguments.Arch != null) 77 | { 78 | return Arguments.Arch; 79 | } 80 | 81 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.X64) 82 | { 83 | return "x86_64"; 84 | } 85 | 86 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.Arm64) 87 | { 88 | return "aarch64"; 89 | } 90 | 91 | if (Runtime.RuntimeArch == System.Runtime.InteropServices.Architecture.X86) 92 | { 93 | return "i686"; 94 | } 95 | 96 | return Runtime.RuntimeArch.ToString().ToLowerInvariant(); 97 | } 98 | } 99 | 100 | /// 101 | /// Implements. 102 | /// 103 | public override string OutputName 104 | { 105 | get { return GetOutputName(true, Architecture, ".flatpak"); } 106 | } 107 | 108 | /// 109 | /// Implements. 110 | /// 111 | public override string BuildAppBin { get; } 112 | 113 | /// 114 | /// Implements. 115 | /// 116 | public override string InstallBin { get; } 117 | 118 | /// 119 | /// Implements. 120 | /// 121 | public override string? ManifestBuildPath { get; } 122 | 123 | /// 124 | /// Implements. 125 | /// 126 | public override string? ManifestContent { get; } 127 | 128 | /// 129 | /// Implements. 130 | /// 131 | public override IReadOnlyCollection PackageCommands { get; } 132 | 133 | /// 134 | /// Implements. 135 | /// 136 | public override bool SupportsStartCommand { get; } = false; 137 | 138 | /// 139 | /// Implements. 140 | /// 141 | public override bool SupportsPostRun { get; } = true; 142 | 143 | private string GetFlatpakManifest() 144 | { 145 | var sb = new StringBuilder(); 146 | 147 | // NOTE. Yaml file must be saved next to BuildRoot directory 148 | sb.AppendLine($"app-id: {Configuration.AppId}"); 149 | sb.AppendLine($"runtime: {Configuration.FlatpakPlatformRuntime}"); 150 | sb.AppendLine($"runtime-version: '{Configuration.FlatpakPlatformVersion}'"); 151 | sb.AppendLine($"sdk: {Configuration.FlatpakPlatformSdk}"); 152 | sb.AppendLine($"command: {InstallExec}"); 153 | sb.AppendLine($"modules:"); 154 | sb.AppendLine($" - name: {Configuration.PackageName}"); 155 | sb.AppendLine($" buildsystem: simple"); 156 | sb.AppendLine($" build-commands:"); 157 | sb.AppendLine($" - mkdir -p /app/bin"); 158 | sb.AppendLine($" - cp -rn bin/* /app/bin"); 159 | sb.AppendLine($" - mkdir -p /app/share"); 160 | sb.AppendLine($" - cp -rn share/* /app/share"); 161 | sb.AppendLine($" sources:"); 162 | sb.AppendLine($" - type: dir"); 163 | sb.AppendLine($" path: {AppRootName}/usr/"); 164 | 165 | if (Configuration.FlatpakFinishArgs.Count != 0) 166 | { 167 | sb.AppendLine($"finish-args:"); 168 | 169 | foreach (var item in Configuration.FlatpakFinishArgs) 170 | { 171 | sb.Append(" - "); 172 | sb.AppendLine(item); 173 | } 174 | } 175 | 176 | return sb.ToString().TrimEnd(); 177 | } 178 | 179 | } 180 | 181 | -------------------------------------------------------------------------------- /PupNet/Builders/ZipBuilder.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet.Builders; 20 | 21 | /// 22 | /// Extends for Zip package. 23 | /// 24 | public class ZipBuilder : PackageBuilder 25 | { 26 | /// 27 | /// Constructor. 28 | /// 29 | public ZipBuilder(ConfigurationReader conf) 30 | : base(conf, PackageKind.Zip) 31 | { 32 | BuildAppBin = Path.Combine(BuildRoot, "Publish"); 33 | 34 | // Not used 35 | InstallBin = ""; 36 | 37 | // Not used 38 | ManifestBuildPath = null; 39 | ManifestContent = null; 40 | PackageCommands = Array.Empty(); 41 | } 42 | 43 | /// 44 | /// Implements. 45 | /// 46 | public override string Architecture 47 | { 48 | get 49 | { 50 | if (Arguments.Arch != null) 51 | { 52 | return Arguments.Arch; 53 | } 54 | 55 | return Runtime.RuntimeArch.ToString().ToLowerInvariant(); 56 | } 57 | } 58 | 59 | /// 60 | /// Implements. 61 | /// 62 | public override string OutputName 63 | { 64 | get { return GetOutputName(true, Runtime.RuntimeId, ".zip"); } 65 | } 66 | 67 | /// 68 | /// Implements. 69 | /// 70 | public override string BuildAppBin { get; } 71 | 72 | /// 73 | /// Implements. 74 | /// 75 | public override string InstallBin { get; } 76 | 77 | /// 78 | /// Implements. 79 | /// 80 | public override string? ManifestBuildPath { get; } 81 | 82 | /// 83 | /// Implements. 84 | /// 85 | public override string? ManifestContent { get; } 86 | 87 | /// 88 | /// Implements. 89 | /// 90 | public override IReadOnlyCollection PackageCommands { get; } 91 | 92 | /// 93 | /// Implements. 94 | /// 95 | public override bool SupportsStartCommand { get; } = false; 96 | 97 | /// 98 | /// Implements. 99 | /// 100 | public override bool SupportsPostRun { get; } = true; 101 | 102 | /// 103 | /// Overrides and extends. 104 | /// 105 | public override void BuildPackage() 106 | { 107 | // Package commands empty - does nothing 108 | base.BuildPackage(); 109 | 110 | Operations.Zip(BuildAppBin, OutputPath); 111 | 112 | if (Arguments.IsRun) 113 | { 114 | // Just run the build 115 | Directory.SetCurrentDirectory(BuildAppBin); 116 | Operations.Execute(AppExecName); 117 | } 118 | } 119 | 120 | } 121 | 122 | -------------------------------------------------------------------------------- /PupNet/ChangeItem.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet; 20 | 21 | /// 22 | /// Immutable change-item class. 23 | /// 24 | public class ChangeItem : IEquatable 25 | { 26 | /// 27 | /// Constructor which sets and to false. 28 | /// 29 | public ChangeItem(string change) 30 | { 31 | Change = change; 32 | } 33 | 34 | /// 35 | /// Constructor which sets , and to true. 36 | /// 37 | public ChangeItem(string version, DateTime date) 38 | { 39 | IsHeader = true; 40 | Version = version; 41 | Date = date; 42 | } 43 | 44 | /// 45 | /// Gets whether this is a header item. 46 | /// 47 | public bool IsHeader { get; } 48 | 49 | /// 50 | /// Gets the header version. It is null where is false, and a valid string when true. 51 | /// 52 | public string? Version { get; } 53 | 54 | /// 55 | /// Gets the header date. It is default where is false, and a valid date value when true. 56 | /// 57 | public DateTime Date { get; } 58 | 59 | /// 60 | /// Gets the change description. It is null where is true, and a valid single-line 61 | /// description where false. 62 | /// 63 | public string? Change { get; } 64 | 65 | /// 66 | /// Implements. 67 | /// 68 | public bool Equals(ChangeItem? other) 69 | { 70 | if (other == null) 71 | { 72 | return false; 73 | } 74 | 75 | return IsHeader == other.IsHeader && Change == other.Change && 76 | Version == other.Version && Date.Equals(other.Date); 77 | } 78 | 79 | /// 80 | /// Overrides. 81 | /// 82 | public override bool Equals(object? other) 83 | { 84 | return Equals(other as ChangeItem); 85 | } 86 | 87 | /// 88 | /// Overrides. 89 | /// 90 | public override int GetHashCode() 91 | { 92 | return HashCode.Combine(Version, Date, Change); 93 | } 94 | } -------------------------------------------------------------------------------- /PupNet/ChangeParser.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Globalization; 20 | using System.Security; 21 | using System.Text; 22 | 23 | namespace KuiperZone.PupNet; 24 | 25 | /// 26 | /// A class which reads changelog information and translates it to a sequence of values. 27 | /// The class is immutable and is given a filename or string content on construction. The content is parsed, and 28 | /// change related information extracted, with superfluous surround information ignored. The input format is form: 29 | /// + 1.0.0;[OthersIgnored;]2023-05-01 30 | /// - Change description 1 31 | /// - Change description 2 32 | /// - etc. 33 | /// + 2.0.0;[OthersIgnored;]2023-05-02] 34 | /// In the above, the first item in the "header" is always the version, and the last always the date, with the ';' being 35 | /// a separator. The date is expected to be in form "yyyy-MM-dd", but can be in any DateTime parsable form. Additional 36 | /// items between these two are ignored. 37 | /// 38 | public class ChangeParser 39 | { 40 | /// 41 | /// Version header prefix character. 42 | /// 43 | public const char HeaderPrefix = '+'; 44 | 45 | /// 46 | /// Version header separator character. 47 | /// 48 | public const char HeaderSeparator = ';'; 49 | 50 | /// 51 | /// Change item prefix character. 52 | /// 53 | public const char ChangePrefix = '-'; 54 | 55 | /// 56 | /// Constructor which reads the file content. Also serves as a default (empty) constructor. 57 | /// 58 | public ChangeParser(string? filename = null) 59 | : this(string.IsNullOrEmpty(filename) ? Array.Empty() : File.ReadAllLines(filename)) 60 | { 61 | } 62 | 63 | /// 64 | /// Constructor with CHANGE file content lines. 65 | /// 66 | public ChangeParser(IEnumerable content) 67 | { 68 | var items = new List(); 69 | Items = items; 70 | 71 | ChangeItem? header = null; 72 | string? change = null; 73 | 74 | foreach (var s in content) 75 | { 76 | var line = s.Trim(); 77 | var tempHeader = TryParseHeader(line); 78 | 79 | if (tempHeader != null) 80 | { 81 | // New header 82 | AppendChange(items, ref change); 83 | 84 | header = tempHeader; 85 | items.Add(header); 86 | continue; 87 | } 88 | 89 | if (header != null) 90 | { 91 | if (line.Length == 0) 92 | { 93 | // An empty line - break current change 94 | // The next line must either be a new header or a new change item 95 | AppendChange(items, ref change); 96 | continue; 97 | } 98 | 99 | // Allow "- description", but not "------" 100 | if (line.StartsWith(ChangePrefix) && !line.StartsWith(new string(ChangePrefix, 2))) 101 | { 102 | // New change item 103 | AppendChange(items, ref change); 104 | change = line.TrimStart(ChangePrefix, ' '); 105 | continue; 106 | } 107 | 108 | if (change != null) 109 | { 110 | // Buffer multiple lines, as long as no empty lines between 111 | change += ' ' + line; 112 | continue; 113 | } 114 | 115 | // Sequence broken, will need a new header to start 116 | header = null; 117 | } 118 | 119 | change = null; 120 | } 121 | 122 | // Append trailing change 123 | AppendChange(items, ref change); 124 | } 125 | 126 | /// 127 | /// Gets the change items. 128 | /// 129 | public IReadOnlyCollection Items { get; } 130 | 131 | /// 132 | /// Overrides. Equivalent to ToString(false). 133 | /// 134 | public override string ToString() 135 | { 136 | return ToTextString(); 137 | } 138 | 139 | /// 140 | /// Returns multiline string output. If appstream is true, the result is formatted for inclusion in AppStream metadata. according to options. 141 | /// /// 142 | public string ToString(bool appstream) 143 | { 144 | if (appstream) 145 | { 146 | return ToAppStreamString(); 147 | } 148 | 149 | return ToTextString(); 150 | } 151 | 152 | private string ToAppStreamString() 153 | { 154 | // Example HTML: 155 | //
    156 | //
  • Bugfix: Fix package creation when file path of contents contain spaces (enclose file path with quotes when executing chmod)
  • 157 | //
158 | bool started = false; 159 | var sb = new StringBuilder(); 160 | 161 | foreach (var item in Items) 162 | { 163 | if (item.IsHeader) 164 | { 165 | if (started) 166 | { 167 | // Terminate last 168 | sb.Append('\n'); 169 | sb.Append(""); 170 | sb.Append("\n\n"); 171 | } 172 | 173 | started = true; 174 | 175 | sb.Append(""); 180 | sb.Append("
    "); 181 | } 182 | else 183 | if (started) 184 | { 185 | sb.Append('\n'); 186 | sb.Append("
  • "); 187 | sb.Append(SecurityElement.Escape(item.Change)); 188 | sb.Append("
  • "); 189 | } 190 | } 191 | 192 | if (started) 193 | { 194 | // Trailing termination 195 | sb.Append('\n'); 196 | sb.Append("
"); 197 | } 198 | 199 | 200 | return sb.ToString(); 201 | } 202 | 203 | private string ToTextString() 204 | { 205 | bool started = false; 206 | var sb = new StringBuilder(); 207 | 208 | foreach (var item in Items) 209 | { 210 | if (item.IsHeader) 211 | { 212 | if (started) 213 | { 214 | // Spacer 215 | sb.Append("\n\n"); 216 | } 217 | 218 | started = true; 219 | 220 | sb.Append(HeaderPrefix); 221 | sb.Append(' '); 222 | sb.Append(item.Version); 223 | 224 | sb.Append(ChangeParser.HeaderSeparator); 225 | sb.Append(item.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); 226 | } 227 | else 228 | if (started) 229 | { 230 | sb.Append('\n'); 231 | sb.Append(ChangePrefix); 232 | sb.Append(' '); 233 | sb.Append(item.Change); 234 | } 235 | } 236 | 237 | return sb.ToString(); 238 | } 239 | 240 | private static void AppendChange(List list, ref string? change) 241 | { 242 | if (!string.IsNullOrEmpty(change)) 243 | { 244 | list.Add(new ChangeItem(change)); 245 | } 246 | 247 | change = null; 248 | } 249 | 250 | private static ChangeItem? TryParseHeader(string line) 251 | { 252 | const int MaxVersion = 25; 253 | 254 | // Allow "+ ", but not "++++" 255 | if (line.StartsWith(HeaderPrefix) && !line.StartsWith(new string(HeaderPrefix, 2))) 256 | { 257 | var items = line.Split(HeaderSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); 258 | 259 | if (items.Length > 1 && DateTime.TryParse(items[^1], CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date)) 260 | { 261 | string version = items[0].TrimStart(HeaderPrefix, ' '); 262 | 263 | if (version.Length > 0 && version.Length <= MaxVersion) 264 | { 265 | return new ChangeItem(version, date); 266 | } 267 | } 268 | } 269 | 270 | return null; 271 | } 272 | 273 | } -------------------------------------------------------------------------------- /PupNet/ComfirmPrompt.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet; 20 | 21 | /// 22 | /// Prompts for yes or no. 23 | /// 24 | public class ConfirmPrompt 25 | { 26 | /// 27 | /// Constructor. 28 | /// 29 | public ConfirmPrompt(bool multi = false) 30 | : this(null, multi) 31 | { 32 | } 33 | 34 | /// 35 | /// Constructor. 36 | /// 37 | public ConfirmPrompt(string? question, bool multi = false) 38 | { 39 | question = question?.Trim(); 40 | 41 | if (string.IsNullOrEmpty(question)) 42 | { 43 | question = "Continue?"; 44 | } 45 | 46 | if (multi) 47 | { 48 | IsMultiple = true; 49 | PromptText += question + " [N/y] or ESC aborts: "; 50 | } 51 | else 52 | { 53 | PromptText = question + " [N/y]: "; 54 | } 55 | } 56 | 57 | /// 58 | /// Gets the prompt text. 59 | /// 60 | public string PromptText { get; } 61 | 62 | /// 63 | /// Multiple prompts (adds Escape option). 64 | /// 65 | public bool IsMultiple { get; } 66 | 67 | /// 68 | /// Gets user response. 69 | /// 70 | public bool Wait() 71 | { 72 | Console.Write(PromptText); 73 | 74 | var key = Console.ReadKey(true).Key; 75 | 76 | if (key == ConsoleKey.Escape) 77 | { 78 | Console.WriteLine(); 79 | throw new InvalidOperationException("Aborted by user"); 80 | } 81 | 82 | if (key == ConsoleKey.Y) 83 | { 84 | Console.WriteLine("Y"); 85 | return true; 86 | } 87 | 88 | Console.WriteLine("N"); 89 | return false; 90 | } 91 | 92 | } 93 | 94 | -------------------------------------------------------------------------------- /PupNet/DocStyles.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet; 20 | 21 | /// 22 | /// Determines documentation string output. 23 | /// 24 | public enum DocStyles 25 | { 26 | /// 27 | /// No documentation. 28 | /// 29 | NoComments = 0, 30 | 31 | /// 32 | /// Verbose output. 33 | /// 34 | Comments, 35 | 36 | /// 37 | /// Reference output. 38 | /// 39 | Reference, 40 | } 41 | 42 | -------------------------------------------------------------------------------- /PupNet/FileOps.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Diagnostics; 20 | using System.IO.Compression; 21 | using System.Runtime.InteropServices; 22 | using Microsoft.VisualBasic.FileIO; 23 | 24 | namespace KuiperZone.PupNet; 25 | 26 | /// 27 | /// Wraps expected file operations with desired console output behavior. 28 | /// This is mainly for convenience and aesthetic/information purposes. 29 | /// 30 | public class FileOps(string? root = null) 31 | { 32 | /// 33 | /// Gets the root directory for operations. This is used for display purposes only where directory path is 34 | /// removed from the displayed path. 35 | /// 36 | public string? Root { get; } = root; 37 | 38 | /// 39 | /// Gets or sets whether to display commands and path information. Default is true. 40 | /// 41 | public bool ShowCommands { get; set; } = true; 42 | 43 | /// 44 | /// Gets a list of files currently under dir, including sub-paths. 45 | /// Output paths are relative to given directory. Does not pick up symlinks. 46 | /// 47 | public static string[] ListFiles(string dir, string filter = "*") 48 | { 49 | var opts = new EnumerationOptions(); 50 | opts.RecurseSubdirectories = true; 51 | opts.ReturnSpecialDirectories = false; 52 | opts.IgnoreInaccessible = true; 53 | opts.MaxRecursionDepth = 20; 54 | 55 | var files = Directory.GetFiles(dir, filter, System.IO.SearchOption.AllDirectories); 56 | 57 | for (int n = 0; n < files.Length; ++n) 58 | { 59 | files[n] = Path.GetRelativePath(dir, files[n]); 60 | } 61 | 62 | return files; 63 | } 64 | 65 | /// 66 | /// Asserts file exist. Does nothing if file is null. 67 | /// 68 | public void AssertExists(string? filepath) 69 | { 70 | if (filepath != null) 71 | { 72 | Write("Exists?: ", filepath); 73 | 74 | if (!File.Exists(filepath)) 75 | { 76 | WriteLine(" ... FAILED"); 77 | throw new FileNotFoundException("File not found " + filepath); 78 | } 79 | 80 | WriteLine(" ... OK"); 81 | } 82 | } 83 | 84 | /// 85 | /// Ensures directory exists. Does nothing if dir is null. 86 | /// 87 | public void CreateDirectory(string? dir) 88 | { 89 | if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) 90 | { 91 | try 92 | { 93 | Write("Create Directory: ", dir); 94 | Directory.CreateDirectory(dir); 95 | WriteLine(" ... OK"); 96 | } 97 | catch 98 | { 99 | WriteLine(" ... FAILED"); 100 | throw; 101 | } 102 | } 103 | } 104 | 105 | /// 106 | /// Ensures directory is deleted (recursive). Does nothing if dir is null. 107 | /// 108 | public void RemoveDirectory(string? dir) 109 | { 110 | if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) 111 | { 112 | try 113 | { 114 | Write("Remove: ", dir); 115 | Directory.Delete(dir, true); 116 | WriteLine(" ... OK"); 117 | } 118 | catch 119 | { 120 | WriteLine(" ... FAILED"); 121 | throw; 122 | } 123 | } 124 | } 125 | 126 | 127 | /// 128 | /// Copies directory. Does not create destination. Does nothing if either value is null. 129 | /// 130 | public void CopyDirectory(string? src, string? dst) 131 | { 132 | if (!string.IsNullOrEmpty(src) && !string.IsNullOrEmpty(dst)) 133 | { 134 | try 135 | { 136 | Write("Populate: ", dst); 137 | 138 | if (!Directory.Exists(dst)) 139 | { 140 | throw new DirectoryNotFoundException("Directory not found " + dst); 141 | } 142 | 143 | FileSystem.CopyDirectory(src, dst); 144 | WriteLine(" ... OK"); 145 | } 146 | catch 147 | { 148 | WriteLine(" ... FAILED"); 149 | throw; 150 | } 151 | } 152 | 153 | } 154 | 155 | /// 156 | /// Copies single single file. Does nothing if either value is null. 157 | /// 158 | public void CopyFile(string? src, string? dst, bool ensureDirectory = false) 159 | { 160 | if (!string.IsNullOrEmpty(src) && !string.IsNullOrEmpty(dst)) 161 | { 162 | if (ensureDirectory) 163 | { 164 | CreateDirectory(Path.GetDirectoryName(dst)); 165 | } 166 | 167 | try 168 | { 169 | Write("Create File: ", dst); 170 | File.Copy(src, dst, true); 171 | WriteLine(" ... OK"); 172 | } 173 | catch 174 | { 175 | WriteLine(" ... FAILED"); 176 | throw; 177 | } 178 | } 179 | } 180 | 181 | /// 182 | /// Writes file content. Does nothing if either value is null. 183 | /// 184 | public void WriteFile(string? path, string? content, bool replace = false) 185 | { 186 | if (!string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(content) && (replace || !File.Exists(path))) 187 | { 188 | try 189 | { 190 | Write("Create File: ", path); 191 | File.WriteAllText(path, content); 192 | WriteLine(" ... OK"); 193 | } 194 | catch 195 | { 196 | WriteLine(" ... FAILED"); 197 | throw; 198 | } 199 | } 200 | } 201 | 202 | /// 203 | /// Zips the directory and writes to output. 204 | /// 205 | public void Zip(string? directory, string? output) 206 | { 207 | if (!string.IsNullOrEmpty(directory) && !string.IsNullOrEmpty(output)) 208 | { 209 | try 210 | { 211 | if (File.Exists(output)) 212 | { 213 | File.Delete(output); 214 | } 215 | 216 | Write("Zip: ", directory); 217 | ZipFile.CreateFromDirectory(directory, output, CompressionLevel.Optimal, false); 218 | WriteLine(" ... OK"); 219 | } 220 | catch 221 | { 222 | WriteLine(" ... FAILED"); 223 | throw; 224 | } 225 | } 226 | } 227 | 228 | /// 229 | /// Runs the command. 230 | /// 231 | public int Execute(string command, bool throwNonZeroExit = true) 232 | { 233 | string? args = null; 234 | int idx = command.IndexOf(' '); 235 | 236 | if (idx > 0) 237 | { 238 | args = command.Substring(idx + 1).Trim(); 239 | command = command.Substring(0, idx).Trim(); 240 | } 241 | 242 | return Execute(command, args, throwNonZeroExit); 243 | } 244 | 245 | /// 246 | /// Runs the command with separate arguments. 247 | /// 248 | public int Execute(string command, string? args, bool throwNonZeroExit = true) 249 | { 250 | bool redirect = false; 251 | string orig = command.ToLowerInvariant(); 252 | 253 | if (orig == "rem" || orig == "::" || orig == "#") 254 | { 255 | // Ignore commands which look like comments 256 | return 0; 257 | } 258 | 259 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 260 | { 261 | redirect = true; 262 | 263 | if (orig != "cmd" && orig != "cmd.exe") 264 | { 265 | // Fix up dos command 266 | args = $"/C {command} {args}"; 267 | command = "cmd"; 268 | } 269 | } 270 | 271 | if (orig != "echo") 272 | { 273 | WriteLine($"{command} {args}"); 274 | } 275 | 276 | var info = new ProcessStartInfo 277 | { 278 | Arguments = args, 279 | CreateNoWindow = true, 280 | FileName = command, 281 | RedirectStandardOutput = redirect, 282 | RedirectStandardError = redirect, 283 | UseShellExecute = false, 284 | }; 285 | 286 | using var proc = Process.Start(info) ?? 287 | throw new InvalidOperationException($"{command} failed"); 288 | 289 | if (redirect) 290 | { 291 | Write(proc.StandardOutput.ReadToEnd()); 292 | Write(proc.StandardError.ReadToEnd()); 293 | } 294 | 295 | proc.WaitForExit(); 296 | 297 | if (throwNonZeroExit && proc.ExitCode != 0) 298 | { 299 | throw new InvalidOperationException($"{command} returned non-zero exit code {proc.ExitCode}"); 300 | } 301 | 302 | return proc.ExitCode; 303 | } 304 | 305 | /// 306 | /// Runs the commands. Does nothing if empty. 307 | /// 308 | public int Execute(IEnumerable commands, bool throwNonZeroExit = true) 309 | { 310 | bool more = false; 311 | 312 | foreach (var item in commands) 313 | { 314 | if (more) 315 | { 316 | WriteLine(null); 317 | } 318 | 319 | more = true; 320 | int rslt = Execute(item, throwNonZeroExit); 321 | 322 | if (rslt != 0) 323 | { 324 | return rslt; 325 | } 326 | } 327 | 328 | return 0; 329 | } 330 | 331 | private void Write(string? prefix, string? path = null) 332 | { 333 | if (ShowCommands) 334 | { 335 | Console.Write(prefix); 336 | 337 | if (path != null && Root != null) 338 | { 339 | path = Path.GetRelativePath(Root, path); 340 | } 341 | 342 | Console.Write(path); 343 | } 344 | } 345 | 346 | private void WriteLine(string? prefix, string? path = null) 347 | { 348 | if (ShowCommands) 349 | { 350 | Write(prefix, path); 351 | Console.WriteLine(); 352 | } 353 | } 354 | 355 | } 356 | 357 | -------------------------------------------------------------------------------- /PupNet/IniReader.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Text; 20 | 21 | namespace KuiperZone.PupNet; 22 | 23 | /// 24 | /// Supported feature flags for . 25 | /// 26 | public enum IniOptions 27 | { 28 | /// 29 | /// Simple key-value strings on single lines. 30 | /// 31 | None = 0x0000, 32 | 33 | /// 34 | /// Strip value of surrounding quote characters. 35 | /// 36 | StripQuotes = 0x0001, 37 | 38 | /// 39 | /// Support multi-line values. 40 | /// 41 | MultiLine = 0x0002, 42 | 43 | /// 44 | /// Default options. 45 | /// 46 | Default = StripQuotes | MultiLine, 47 | } 48 | 49 | /// 50 | /// Reads simple INI content providing a name-value dictionary. 51 | /// 52 | public class IniReader 53 | { 54 | /// 55 | /// Multi-line value start quote. 56 | /// 57 | public const string StartMultiQuote = "\"\"\""; 58 | 59 | /// 60 | /// Multi-line value end quote. 61 | /// 62 | public const string EndMultiQuote = "\"\"\""; 63 | 64 | private readonly string _string = ""; 65 | 66 | /// 67 | /// Default constructor (empty). 68 | /// 69 | public IniReader(IniOptions opts = IniOptions.Default) 70 | { 71 | Options = opts; 72 | Values = new Dictionary(); 73 | } 74 | 75 | /// 76 | /// Constructor with multi-line content. 77 | /// 78 | public IniReader(string path, IniOptions opts = IniOptions.Default) 79 | { 80 | Options = opts; 81 | Filepath = Path.GetFullPath(path); 82 | 83 | var lines = File.ReadAllLines(Filepath); 84 | Values = Parse(lines); 85 | _string = string.Join('\n', lines).Trim(); 86 | } 87 | 88 | /// 89 | /// Constructor with content lines. 90 | /// 91 | public IniReader(string[] lines, IniOptions opts = IniOptions.Default) 92 | { 93 | Options = opts; 94 | Values = Parse(lines); 95 | _string = string.Join('\n', lines).Trim(); 96 | } 97 | 98 | /// 99 | /// Gets the file path. 100 | /// 101 | public string Filepath { get; } = ""; 102 | 103 | /// 104 | /// Supports multiple 105 | /// 106 | public IniOptions Options { get; } 107 | 108 | /// 109 | /// Gets the values. The key is ordinal case insensitive. 110 | /// 111 | public IReadOnlyDictionary Values { get; } 112 | 113 | /// 114 | /// Overrides. 115 | /// 116 | public override string ToString() 117 | { 118 | return _string; 119 | } 120 | 121 | private Dictionary Parse(string[] content) 122 | { 123 | int n = 0; 124 | var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); 125 | 126 | while (n < content.Length) 127 | { 128 | var hold = n; 129 | var name = ParseNameValue(content, ref n, out string value); 130 | 131 | if (name.Length != 0 && !dict.TryAdd(name, value)) 132 | { 133 | throw new ArgumentException(GetError($"Repeated key {name}", hold)); 134 | } 135 | 136 | 137 | } 138 | 139 | return dict; 140 | } 141 | 142 | private string ParseNameValue(string[] content, ref int num, out string value) 143 | { 144 | int hold = num; 145 | var line = content[num++].Trim(); 146 | 147 | if (line.Length == 0 || line.StartsWith('#') || line.StartsWith("//")) 148 | { 149 | // Comment or empty 150 | value = ""; 151 | return ""; 152 | } 153 | 154 | int pos = line.IndexOf('='); 155 | 156 | if (pos > 0) 157 | { 158 | var name = line.Substring(0, pos).Trim(); 159 | value = line.Substring(pos + 1).Trim(); 160 | 161 | if (Options.HasFlag(IniOptions.MultiLine) && value.StartsWith(StartMultiQuote)) 162 | { 163 | var sb = new StringBuilder(1024); 164 | line = value.Substring(StartMultiQuote.Length); 165 | 166 | while (true) 167 | { 168 | // Never want surrounding spaces or tabs 169 | line = line.Trim().Replace("\t", " "); 170 | 171 | pos = line.IndexOf(EndMultiQuote); 172 | 173 | if (pos > -1) 174 | { 175 | _ = sb.Append(line.AsSpan(0, pos)); 176 | value = sb.ToString().Trim(); 177 | return name; 178 | } 179 | 180 | sb.Append(line); 181 | sb.Append('\n'); 182 | 183 | if (num < content.Length) 184 | { 185 | line = content[num++]; 186 | continue; 187 | } 188 | 189 | throw new ArgumentException(GetError("No multi-line termination", hold)); 190 | } 191 | } 192 | else 193 | if (Options.HasFlag(IniOptions.StripQuotes) && value.Length > 1) 194 | { 195 | if ((value.StartsWith('"') && value.EndsWith('"')) || 196 | (value.StartsWith('\'') && value.EndsWith('\''))) 197 | { 198 | value = value.Substring(1, value.Length - 2); 199 | } 200 | } 201 | 202 | return name; 203 | } 204 | 205 | throw new ArgumentException(GetError("Syntax error", hold)); 206 | } 207 | 208 | private string GetError(string msg, int num) 209 | { 210 | if (!string.IsNullOrEmpty(Filepath)) 211 | { 212 | throw new ArgumentException($"{msg} in {Path.GetFileName(Filepath)} at #{num}"); 213 | } 214 | 215 | throw new ArgumentException($"{msg} at #{num}"); 216 | } 217 | } -------------------------------------------------------------------------------- /PupNet/MacroId.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | namespace KuiperZone.PupNet; 20 | 21 | /// 22 | /// Defines expandable macros. 23 | /// 24 | public enum MacroId 25 | { 26 | LocalDirectory, 27 | AppBaseName, 28 | AppFriendlyName, 29 | AppId, 30 | AppShortSummary, 31 | AppLicenseId, 32 | PublisherName, 33 | PublisherCopyright, 34 | PublisherLinkName, 35 | PublisherLinkUrl, 36 | PublisherEmail, 37 | DesktopNoDisplay, 38 | DesktopIntegrate, 39 | DesktopTerminal, 40 | PrimeCategory, 41 | 42 | AppStreamDescriptionXml, 43 | AppStreamChangelogXml, 44 | AppVersion, 45 | PackageRelease, 46 | DeployKind, 47 | DotnetRuntime, 48 | BuildArch, 49 | BuildTarget, 50 | BuildDate, 51 | BuildYear, 52 | BuildRoot, 53 | BuildShare, 54 | BuildAppBin, 55 | 56 | InstallBin, 57 | InstallExec, 58 | } 59 | 60 | /// 61 | /// Extension methods. 62 | /// 63 | public static class MacroIdExtension 64 | { 65 | /// 66 | /// Converts to name string (i.e. "APP_BASE_NAME"). 67 | /// 68 | public static string ToName(this MacroId id) 69 | { 70 | // Do not change names as will break configs out in the wild 71 | switch (id) 72 | { 73 | // Direct from config 74 | case MacroId.LocalDirectory: return "LOCAL_DIRECTORY"; 75 | case MacroId.AppBaseName: return "APP_BASE_NAME"; 76 | case MacroId.AppFriendlyName: return "APP_FRIENDLY_NAME"; 77 | case MacroId.AppId: return "APP_ID"; 78 | case MacroId.AppShortSummary: return "APP_SHORT_SUMMARY"; 79 | case MacroId.AppLicenseId: return "APP_LICENSE_ID"; 80 | case MacroId.PublisherName: return "PUBLISHER_NAME"; 81 | case MacroId.PublisherCopyright: return "PUBLISHER_COPYRIGHT"; 82 | case MacroId.PublisherLinkName: return "PUBLISHER_LINK_NAME"; 83 | case MacroId.PublisherLinkUrl: return "PUBLISHER_LINK_URL"; 84 | case MacroId.PublisherEmail: return "PUBLISHER_EMAIL"; 85 | case MacroId.DesktopNoDisplay: return "DESKTOP_NODISPLAY"; 86 | case MacroId.DesktopIntegrate: return "DESKTOP_INTEGRATE"; 87 | case MacroId.DesktopTerminal: return "DESKTOP_TERMINAL"; 88 | case MacroId.PrimeCategory: return "PRIME_CATEGORY"; 89 | 90 | // Others 91 | case MacroId.AppStreamDescriptionXml: return "APPSTREAM_DESCRIPTION_XML"; 92 | case MacroId.AppStreamChangelogXml: return "APPSTREAM_CHANGELOG_XML"; 93 | case MacroId.AppVersion: return "APP_VERSION"; 94 | case MacroId.PackageRelease: return "PACKAGE_RELEASE"; 95 | case MacroId.DeployKind: return "DEPLOY_KIND"; 96 | case MacroId.DotnetRuntime: return "DOTNET_RUNTIME"; 97 | case MacroId.BuildArch: return "BUILD_ARCH"; 98 | case MacroId.BuildTarget: return "BUILD_TARGET"; 99 | case MacroId.BuildDate: return "BUILD_DATE"; 100 | case MacroId.BuildYear: return "BUILD_YEAR"; 101 | case MacroId.BuildRoot: return "BUILD_ROOT"; 102 | case MacroId.BuildShare: return "BUILD_SHARE"; 103 | case MacroId.BuildAppBin: return "BUILD_APP_BIN"; 104 | 105 | // Install locations 106 | case MacroId.InstallBin: return "INSTALL_BIN"; 107 | case MacroId.InstallExec: return "INSTALL_EXEC"; 108 | 109 | default: throw new ArgumentException("Unknown macro " + id); 110 | } 111 | } 112 | 113 | /// 114 | /// Returns true if the macro value may contain XML and/or other key names. 115 | /// 116 | public static bool ContainsXml(this MacroId id) 117 | { 118 | return id == MacroId.AppStreamDescriptionXml || id == MacroId.AppStreamChangelogXml; 119 | } 120 | 121 | /// 122 | /// Converts to variable string (i.e. "${APP_BASE_NAME}"). 123 | /// 124 | public static string ToVar(this MacroId id) 125 | { 126 | return "${" + ToName(id) + "}"; 127 | } 128 | 129 | public static string ToHint(this MacroId id) 130 | { 131 | switch (id) 132 | { 133 | case MacroId.LocalDirectory: return $"The pupnet.conf file directory"; 134 | case MacroId.AppBaseName: return GetConfHelp(nameof(ConfigurationReader.AppBaseName)); 135 | case MacroId.AppFriendlyName: return GetConfHelp(nameof(ConfigurationReader.AppFriendlyName)); 136 | case MacroId.AppId: return GetConfHelp(nameof(ConfigurationReader.AppId)); 137 | case MacroId.AppShortSummary: return GetConfHelp(nameof(ConfigurationReader.AppShortSummary)); 138 | case MacroId.AppLicenseId: return GetConfHelp(nameof(ConfigurationReader.AppLicenseId)); 139 | 140 | case MacroId.PublisherName: return GetConfHelp(nameof(ConfigurationReader.PublisherName)); 141 | case MacroId.PublisherCopyright: return GetConfHelp(nameof(ConfigurationReader.PublisherCopyright)); 142 | case MacroId.PublisherLinkName: return GetConfHelp(nameof(ConfigurationReader.PublisherLinkName)); 143 | case MacroId.PublisherLinkUrl: return GetConfHelp(nameof(ConfigurationReader.PublisherLinkUrl)); 144 | case MacroId.PublisherEmail: return GetConfHelp(nameof(ConfigurationReader.PublisherEmail)); 145 | 146 | case MacroId.DesktopNoDisplay: return GetConfHelp(nameof(ConfigurationReader.DesktopNoDisplay)); 147 | case MacroId.DesktopTerminal: return GetConfHelp(nameof(ConfigurationReader.DesktopTerminal)); 148 | case MacroId.PrimeCategory: return GetConfHelp(nameof(ConfigurationReader.PrimeCategory)); 149 | 150 | case MacroId.DesktopIntegrate: return $"Gives the logical not of {MacroId.DesktopNoDisplay.ToVar()}"; 151 | 152 | case MacroId.AppStreamDescriptionXml: return "AppStream application description XML (use within the element only)"; 153 | case MacroId.AppStreamChangelogXml: return "AppStream changelog XML content (use within the element only)"; 154 | case MacroId.AppVersion: return "Application version, excluding package-release extension"; 155 | case MacroId.PackageRelease: return "Package release version"; 156 | case MacroId.DeployKind: return "Deployment output kind: appimage, flatpak, rpm, deb, setup, zip"; 157 | case MacroId.DotnetRuntime: return "Dotnet publish runtime identifier used (RID)"; 158 | 159 | case MacroId.BuildArch: return "Build architecture: x64, arm64, arm or x86 (may differ from package output notation)"; 160 | case MacroId.BuildTarget: return "Release or Debug (Release unless explicitly specified)"; 161 | case MacroId.BuildDate: return "Build date in 'yyyy-MM-dd' format"; 162 | case MacroId.BuildYear: return "Build year as 'yyyy'"; 163 | case MacroId.BuildRoot: return "Root of the temporary application build directory"; 164 | case MacroId.BuildShare: return $"Linux 'usr/share' build directory under {nameof(MacroId.BuildRoot)} (empty for some deployments)"; 165 | case MacroId.BuildAppBin: return "Application build directory (i.e. the output of dotnet publish or C++ make)"; 166 | 167 | case MacroId.InstallBin: return "Path to application directory on target system (not the build system)"; 168 | case MacroId.InstallExec: return "Path to application executable on target system (not the build system)"; 169 | 170 | default: throw new ArgumentException("Unknown macro " + id); 171 | } 172 | } 173 | 174 | private static string GetConfHelp(string name) 175 | { 176 | return $"Gives the {name} value from the pupnet.conf file"; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /PupNet/MetaTemplates.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Text; 20 | 21 | namespace KuiperZone.PupNet; 22 | 23 | /// 24 | /// Static templates for desktop and AppStream metainfo files. 25 | /// 26 | public static class MetaTemplates 27 | { 28 | /// 29 | /// Gets the desktop file template. 30 | /// 31 | public static string Desktop { get; } = GetDesktopTemplate(); 32 | 33 | /// 34 | /// Gets the AppStream metadata template. Contains macro variables. 35 | /// 36 | public static string MetaInfo { get; } = GetMetaInfoTemplate(); 37 | 38 | private static string GetDesktopTemplate() 39 | { 40 | var list = new List(); 41 | list.Add("[Desktop Entry]"); 42 | list.Add($"Type=Application"); 43 | list.Add($"Name={MacroId.AppFriendlyName.ToVar()}"); 44 | list.Add($"Icon={MacroId.AppId.ToVar()}"); 45 | list.Add($"Comment={MacroId.AppShortSummary.ToVar()}"); 46 | list.Add($"Exec={MacroId.InstallExec.ToVar()}"); 47 | list.Add($"TryExec={MacroId.InstallExec.ToVar()}"); 48 | list.Add($"NoDisplay={MacroId.DesktopNoDisplay.ToVar()}"); 49 | list.Add($"X-AppImage-Integrate={MacroId.DesktopIntegrate.ToVar()}"); 50 | list.Add($"Terminal={MacroId.DesktopTerminal.ToVar()}"); 51 | list.Add($"Categories={MacroId.PrimeCategory.ToVar()};"); 52 | list.Add($"MimeType="); 53 | list.Add($"Keywords="); 54 | 55 | return string.Join('\n', list); 56 | } 57 | 58 | private static string GetMetaInfoTemplate(bool comments = true) 59 | { 60 | const string IndentX1 = " "; 61 | const string IndentX2 = IndentX1 + " "; 62 | const string IndentX3 = IndentX2 + " "; 63 | const string IndentX4 = IndentX3 + " "; 64 | const string IndentX5 = IndentX4 + " "; 65 | 66 | var sb = new StringBuilder(); 67 | sb.AppendLine($""); 68 | sb.AppendLine($""); 69 | sb.AppendLine($"{IndentX1}MIT"); 70 | sb.AppendLine(); 71 | 72 | if (comments) 73 | { 74 | sb.AppendLine($"{IndentX1}"); 75 | } 76 | 77 | sb.AppendLine($"{IndentX1}{MacroId.AppId.ToVar()}"); 78 | sb.AppendLine($"{IndentX1}{MacroId.AppFriendlyName.ToVar()}"); 79 | sb.AppendLine($"{IndentX1}{MacroId.AppShortSummary.ToVar()}"); 80 | sb.AppendLine($"{IndentX1}{MacroId.PublisherName.ToVar()}"); 81 | sb.AppendLine($"{IndentX1}{MacroId.PublisherLinkUrl.ToVar()}"); 82 | sb.AppendLine($"{IndentX1}{MacroId.AppLicenseId.ToVar()}"); 83 | sb.AppendLine($"{IndentX1}"); 84 | sb.AppendLine(); 85 | sb.AppendLine($"{IndentX1}{MacroId.AppId.ToVar()}.desktop"); 86 | sb.AppendLine(); 87 | sb.AppendLine($"{IndentX1}"); 88 | 89 | if (comments) 90 | { 91 | sb.AppendLine($"{IndentX2}"); 92 | } 93 | 94 | sb.AppendLine($"{IndentX2}{MacroId.AppStreamDescriptionXml.ToVar()}"); 95 | 96 | if (comments) 97 | { 98 | sb.AppendLine($"{IndentX2}"); 106 | } 107 | 108 | sb.AppendLine($"{IndentX1}"); 109 | sb.AppendLine(); 110 | sb.AppendLine($"{IndentX1}"); 111 | sb.AppendLine($"{IndentX1}"); 112 | sb.AppendLine($"{IndentX2}{MacroId.PrimeCategory.ToVar()}"); 113 | sb.AppendLine($"{IndentX1}"); 114 | sb.AppendLine(); 115 | 116 | if (comments) 117 | { 118 | sb.AppendLine($"{IndentX1}"); 127 | sb.AppendLine(); 128 | sb.AppendLine($"{IndentX1}"); 135 | sb.AppendLine(); 136 | } 137 | 138 | sb.AppendLine($"{IndentX1}"); 139 | 140 | if (comments) 141 | { 142 | sb.AppendLine($"{IndentX2}"); 143 | } 144 | 145 | sb.AppendLine($"{IndentX2}{MacroId.AppStreamChangelogXml.ToVar()}"); 146 | 147 | if (comments) 148 | { 149 | sb.AppendLine($"{IndentX2}"); 159 | } 160 | 161 | sb.AppendLine($"{IndentX1}"); 162 | sb.AppendLine(); 163 | sb.AppendLine($""); 164 | 165 | return sb.ToString(); 166 | } 167 | 168 | } 169 | 170 | -------------------------------------------------------------------------------- /PupNet/PackageKind.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Runtime.InteropServices; 20 | 21 | namespace KuiperZone.PupNet; 22 | 23 | /// 24 | /// Defines deployable package kinds. 25 | /// 26 | public enum PackageKind 27 | { 28 | /// 29 | /// Simple zip. All platforms. 30 | /// 31 | Zip = 0, 32 | 33 | /// 34 | /// AppImage. Linux only. 35 | /// 36 | AppImage, 37 | 38 | /// 39 | /// Debian package. Linux only. 40 | /// 41 | Deb, 42 | 43 | /// 44 | /// RPM package. Linux only. 45 | /// 46 | Rpm, 47 | 48 | /// 49 | /// Flatpak. Linux only. 50 | /// 51 | Flatpak, 52 | 53 | /// 54 | /// Setup file. Windows only. 55 | /// 56 | Setup, 57 | } 58 | 59 | /// 60 | /// Extension methods. 61 | /// 62 | public static class DeployKindExtension 63 | { 64 | /// 65 | /// Gets file extension. 66 | /// 67 | public static string GetFileExt(this PackageKind kind) 68 | { 69 | switch (kind) 70 | { 71 | case PackageKind.Zip: return ".zip"; 72 | case PackageKind.AppImage: return ".AppImage"; 73 | case PackageKind.Deb: return ".deb"; 74 | case PackageKind.Rpm: return ".rpm"; 75 | case PackageKind.Flatpak: return ".flatpak"; 76 | case PackageKind.Setup: return ".exe"; 77 | default: throw new ArgumentException($"Invalid {nameof(PackageKind)} {kind}"); 78 | } 79 | } 80 | 81 | /// 82 | /// Gets whether compatible with linux. 83 | /// 84 | public static bool TargetsLinux(this PackageKind kind, bool exclusive = false) 85 | { 86 | switch (kind) 87 | { 88 | case PackageKind.Zip: 89 | case PackageKind.AppImage: 90 | case PackageKind.Deb: 91 | case PackageKind.Rpm: 92 | case PackageKind.Flatpak: 93 | return !exclusive || (!TargetsWindows(kind) && !TargetsOsx(kind)); 94 | default: 95 | return false; 96 | } 97 | } 98 | 99 | /// 100 | /// Gets whether compatible with windows. 101 | /// 102 | public static bool TargetsWindows(this PackageKind kind, bool exclusive = false) 103 | { 104 | if (kind == PackageKind.Zip || kind == PackageKind.Setup) 105 | { 106 | return !exclusive || (!TargetsLinux(kind) && !TargetsOsx(kind)); 107 | } 108 | 109 | return false; 110 | } 111 | 112 | /// 113 | /// Gets whether compatible with OSX. 114 | /// 115 | public static bool TargetsOsx(this PackageKind kind, bool exclusive = false) 116 | { 117 | if (kind == PackageKind.Zip) 118 | { 119 | return !exclusive || (!TargetsLinux(kind) && !TargetsOsx(kind)); 120 | } 121 | 122 | return false; 123 | } 124 | 125 | /// 126 | /// Returns true if the package kind can be built on this system. 127 | /// 128 | public static bool CanBuildOnSystem(this PackageKind kind) 129 | { 130 | if (kind.TargetsLinux() && RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 131 | { 132 | return true; 133 | } 134 | 135 | if (kind.TargetsWindows() && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 136 | { 137 | return true; 138 | } 139 | 140 | if (kind.TargetsOsx() && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 141 | { 142 | return true; 143 | } 144 | 145 | return false; 146 | } 147 | 148 | } -------------------------------------------------------------------------------- /PupNet/PupNet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | true 10 | pupnet 11 | ../Deploy/OUT 12 | 13 | true 14 | Assets/app.ico 15 | 16 | KuiperZone.PupNet 17 | PupNet Deploy 18 | Andy Thomas 19 | © Andy Thomas 2022-24 20 | KuiperZone 21 | Publish, Package and Deploy as: AppImage, Windows Setup, Flatpak, Deb, RPM and Zip 22 | publish;pack;deploy;AppImage;flatpak;deb;rpm;setup;installer;linux; 23 | en-US 24 | https://github.com/kuiperzone/PupNet-Deploy 25 | AGPL-3.0-or-later 26 | README.md 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | PreserveNewest 43 | 44 | 45 | PreserveNewest 46 | 47 | 48 | 49 | 50 | PreserveNewest 51 | 52 | 53 | PreserveNewest 54 | 55 | 56 | PreserveNewest 57 | 58 | 59 | 60 | PreserveNewest 61 | 62 | 63 | PreserveNewest 64 | 65 | 66 | PreserveNewest 67 | 68 | 69 | PreserveNewest 70 | 71 | 72 | PreserveNewest 73 | 74 | 75 | PreserveNewest 76 | 77 | 78 | PreserveNewest 79 | 80 | 81 | PreserveNewest 82 | 83 | 84 | PreserveNewest 85 | 86 | 87 | 88 | PreserveNewest 89 | 90 | 91 | PreserveNewest 92 | 93 | 94 | PreserveNewest 95 | 96 | 97 | PreserveNewest 98 | 99 | 100 | PreserveNewest 101 | 102 | 103 | PreserveNewest 104 | 105 | 106 | PreserveNewest 107 | 108 | 109 | PreserveNewest 110 | 111 | 112 | PreserveNewest 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /PupNet/RuntimeConverter.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // PROJECT : PupNet 3 | // COPYRIGHT : Andy Thomas (C) 2022-24 4 | // LICENSE : GPL-3.0-or-later 5 | // HOMEPAGE : https://github.com/kuiperzone/PupNet 6 | // 7 | // PupNet is free software: you can redistribute it and/or modify it under 8 | // the terms of the GNU Affero General Public License as published by the Free Software 9 | // Foundation, either version 3 of the License, or (at your option) any later version. 10 | // 11 | // PupNet is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License along 16 | // with PupNet. If not, see . 17 | // ----------------------------------------------------------------------------- 18 | 19 | using System.Runtime.InteropServices; 20 | 21 | namespace KuiperZone.PupNet; 22 | 23 | /// 24 | /// Converts dotnet publish "runtime" ("-r") value into value. 25 | /// 26 | public class RuntimeConverter 27 | { 28 | /// 29 | /// Static constructor. 30 | /// 31 | static RuntimeConverter() 32 | { 33 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 34 | { 35 | SystemOS = OSPlatform.Windows; 36 | 37 | if (RuntimeInformation.OSArchitecture == Architecture.Arm64) 38 | { 39 | DefaultRuntime = "win-arm64"; 40 | } 41 | else 42 | { 43 | DefaultRuntime = "win-x64"; 44 | } 45 | } 46 | else 47 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 48 | { 49 | SystemOS = OSPlatform.OSX; 50 | DefaultRuntime = "osx-x64"; 51 | } 52 | else 53 | { 54 | if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) 55 | { 56 | SystemOS = OSPlatform.FreeBSD; 57 | } 58 | else 59 | { 60 | SystemOS = OSPlatform.Linux; 61 | } 62 | 63 | if (RuntimeInformation.OSArchitecture == Architecture.Arm64) 64 | { 65 | DefaultRuntime = "linux-arm64"; 66 | } 67 | else 68 | { 69 | DefaultRuntime = "linux-x64"; 70 | } 71 | } 72 | } 73 | 74 | /// 75 | /// Constructor with dotnet runtime-id value. If not empty, extracts target CPU architecture. 76 | /// If null of empty, defaults to development arch. 77 | /// 78 | public RuntimeConverter(string? runtime) 79 | { 80 | if (!string.IsNullOrEmpty(runtime)) 81 | { 82 | RuntimeId = runtime.ToLowerInvariant(); 83 | } 84 | 85 | // Common rids include: linux-x64, linux-arm64, win-x64 etc. 86 | // Going to work for: X64, Arm64, Arm, X86 87 | // If not matched, leave at system arch. 88 | foreach (var item in Enum.GetValues()) 89 | { 90 | if (RuntimeId.EndsWith("-" + item.ToString().ToLowerInvariant())) 91 | { 92 | RuntimeArch = item; 93 | IsArchUncertain = false; 94 | break; 95 | } 96 | } 97 | 98 | if (RuntimeId.StartsWith("linux") || RuntimeId.StartsWith("rhel") || RuntimeId.StartsWith("tizen")) 99 | { 100 | IsLinuxRuntime = true; 101 | DefaultPackage = PackageKind.AppImage.CanBuildOnSystem() ? PackageKind.AppImage : PackageKind.Zip; 102 | } 103 | else 104 | if (RuntimeId.StartsWith("win")) 105 | { 106 | IsWindowsRuntime = true; 107 | DefaultPackage = PackageKind.Setup.CanBuildOnSystem() ? PackageKind.Setup : PackageKind.Zip; 108 | } 109 | else 110 | if (RuntimeId.StartsWith("osx")) 111 | { 112 | IsOsxRuntime = true; 113 | DefaultPackage = PackageKind.Zip; 114 | } 115 | else 116 | { 117 | DefaultPackage = PackageKind.Zip; 118 | } 119 | } 120 | 121 | /// 122 | /// Gets system OS, i.e. "Windows", "Linux" or "OSX". 123 | /// 124 | public static OSPlatform SystemOS { get; } 125 | 126 | /// 127 | /// Gets the default runtime. 128 | /// 129 | public static string DefaultRuntime { get; } 130 | 131 | /// 132 | /// Gets the dotnet publish runtime ID (rid) value. 133 | /// 134 | public string RuntimeId { get; } = DefaultRuntime; 135 | 136 | /// 137 | /// Convenience. Gets whether is linux. 138 | /// 139 | public bool IsLinuxRuntime { get; } 140 | 141 | /// 142 | /// Convenience. Gets whether is windows. 143 | /// 144 | public bool IsWindowsRuntime { get; } 145 | 146 | /// 147 | /// Convenience. Gets whether is for OSX. 148 | /// 149 | public bool IsOsxRuntime { get; } 150 | 151 | /// 152 | /// Gets the runtime converted to . 153 | /// 154 | public Architecture RuntimeArch { get; } = RuntimeInformation.OSArchitecture; 155 | 156 | /// 157 | /// Gets whether runtime-id could NOT be mapped to with certainty. 158 | /// 159 | public bool IsArchUncertain { get; } = true; 160 | 161 | /// 162 | /// Gets default package kind given runtime-id. 163 | /// 164 | public PackageKind DefaultPackage { get; } 165 | 166 | /// 167 | /// Converts all known strings to , i.e. "aarch64" to . 168 | /// 169 | /// 170 | public static Architecture ToArchitecture(string arch) 171 | { 172 | arch = arch?.Trim().ToLowerInvariant() ?? throw new ArgumentNullException(nameof(arch)); 173 | 174 | foreach (var item in Enum.GetValues()) 175 | { 176 | // X86, X64, Arm, Arm64 etc. 177 | if (arch.Equals(item.ToString(), StringComparison.OrdinalIgnoreCase)) 178 | { 179 | return item; 180 | } 181 | } 182 | 183 | // Variations 184 | if (arch == "x86_64") 185 | { 186 | return Architecture.X64; 187 | } 188 | 189 | if (arch == "aarch64" || arch == "arm_aarch64") 190 | { 191 | return Architecture.Arm64; 192 | } 193 | 194 | if (arch == "i686") 195 | { 196 | return Architecture.X86; 197 | } 198 | 199 | if (arch == "armhf") 200 | { 201 | return Architecture.Arm; 202 | } 203 | 204 | throw new ArgumentException($"Unknown or unsupported architecture name {arch}"); 205 | } 206 | 207 | /// 208 | /// Returns RuntimeId. 209 | /// 210 | public override string ToString() 211 | { 212 | return RuntimeId; 213 | } 214 | } 215 | 216 | -------------------------------------------------------------------------------- /README.nuget.md: -------------------------------------------------------------------------------- 1 | # PupNet Deploy - Publish & Package for .NET # 2 | 3 | ## Introduction ## 4 | 5 | **PupNet Deploy** is a cross-platform deployment utility which packages your .NET project as a ready-to-ship 6 | installation file in a single step. It is not to be confused with the `dotnet pack` command. 7 | 8 | It has been possible to cross-compile console C# applications for sometime now. More recently, the cross-platform 9 | [Avalonia](https://github.com/AvaloniaUI/Avalonia) replacement for WPF allows fully-featured GUI applications to 10 | target a range of platforms, including: Linux, Windows, MacOS and Android. 11 | 12 | Now, **PupNet Deploy** allows you to ship your dotnet application as: 13 | 14 | * AppImage for Linux 15 | * Setup File for Windows 16 | * Flatpak for Linux 17 | * Debian Binary Package 18 | * RPM Binary Package 19 | * Plain old Zip 20 | 21 | Out of the box, PupNet can create AppImages on Linux and Zip files on all platforms. In order to build other deployments 22 | however, you must first install the appropriate third-party builder tool against which PupNet will call. 23 | 24 | ## Getting Started ## 25 | For instructions on use, see: **[github.com/kuiperzone/PupNet](https://github.com/kuiperzone/PupNet)** 26 | 27 | To install as a dotnet tool: 28 | 29 | dotnet tool install -g KuiperZone.PupNet 30 | 31 | Alternatively, for self-contained installers go: 32 | 33 | **[DOWNLOAD & INSTALL](https://github.com/kuiperzone/PupNet/releases/latest)** 34 | 35 | *If you like this project, don't forget to like and share.* 36 | 37 | ## Copyright & License ## 38 | 39 | Copyright (C) Andy Thomas, 2024. Website: https://kuiper.zone 40 | 41 | PupNet is free software: you can redistribute it and/or modify it under 42 | the terms of the GNU Affero General Public License as published by the Free Software 43 | Foundation, either version 3 of the License, or (at your option) any later version. 44 | 45 | PupNet is distributed in the hope that it will be useful, but WITHOUT 46 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 47 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 48 | 49 | You should have received a copy of the GNU Affero General Public License along 50 | with PupNet. If not, see . 51 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Add: 2 | // https://github.com/dotnet/docs/blob/main/docs/core/tools/dotnet-environment-variables.md#dotnet_host_path 3 | var dotnet = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); 4 | 5 | https://github.com/kuiperzone/PupNet-Deploy/issues/30 6 | 7 | NETSDK1194: The "--output" option isn't supported 8 | See: https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/7.0/solution-level-output-no-longer-valid 9 | 10 | Add multiple --kind values to arguments? 11 | Implement auto system test for maximal and minimal conf files 12 | 13 | Support: nupkg, rpm(xlinux-64), deb(linux-x64), appimage(linux-x64, arm, arm64) and setup (win-x64) 14 | 15 | 16 | RpmRequires 17 | DebRecommends 18 | 19 | 20 | HANDY 21 | dotnet pack -c Release -o ./Deploy/OUT -p:Version=1.4.1 22 | dotnet tool install KuiperZone.PupNet -g --add-source ./Deploy/OUT 23 | dotnet tool uninstall KuiperZone.PupNet -g 24 | dotnet tool update KuiperZone.PupNet -g --add-source ./Deploy/OUT 25 | 26 | 27 | DONE 28 | Add
    parsing to AppDescription 29 | Add parsable "changes" and integrate with RPM,deb and AppStream 30 | Drop to .NET6 for tool 31 | RPM "AutoReqProv" - investigate? 32 | Remove RPM desktop-file-validate or add to "BuildRequires" 33 | Add "Requires" conf section for RPM and DEB: 34 | https://github.com/kuiperzone/PupNet-Deploy/issues/10 35 | https://learn.microsoft.com/en-us/dotnet/core/install/linux-scripted-manual#rpm-dependencies 36 | 37 | Currently, ship runtime from: 38 | https://github.com/AppImage/type2-runtime/releases/tag/continuous 39 | as hinted at on AppImageKit page 40 | 41 | However, think we should use runtimes from: 42 | https://github.com/AppImage/AppImageKit/releases/tag/13 43 | 44 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | dotnet pack -c Release -o ./Deploy/OUT -p:Version=1.8.0 2 | pupnet -r linux-x64 -k deb -y 3 | pupnet -r linux-x64 -k rpm -y 4 | pupnet -r linux-x64 -k appimage -y 5 | pupnet -r linux-arm64 -k appimage -y 6 | pupnet -r linux-arm -k appimage -y 7 | --------------------------------------------------------------------------------