();
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 |
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 |
--------------------------------------------------------------------------------