@@ -77,199 +77,6 @@ public void applyTo(JPackageCommand cmd) throws IOException {
77
77
}
78
78
}
79
79
80
- private static class WinIconVerifier {
81
-
82
- void verifyLauncherIcon (JPackageCommand cmd , String launcherName ,
83
- Path expectedIcon , boolean expectedDefault ) {
84
- TKit .withTempDirectory ("icons" , tmpDir -> {
85
- Path launcher = cmd .appLauncherPath (launcherName );
86
- Path iconWorkDir = tmpDir .resolve (launcher .getFileName ());
87
- Path iconContainer = iconWorkDir .resolve ("container.exe" );
88
- Files .createDirectories (iconContainer .getParent ());
89
- Files .copy (getDefaultAppLauncher (expectedIcon == null
90
- && !expectedDefault ), iconContainer );
91
- if (expectedIcon != null ) {
92
- Executor .tryRunMultipleTimes (() -> {
93
- setIcon (expectedIcon , iconContainer );
94
- }, 3 , 5 );
95
- }
96
-
97
- Path extractedExpectedIcon = extractIconFromExecutable (
98
- iconWorkDir , iconContainer , "expected" );
99
- Path extractedActualIcon = extractIconFromExecutable (iconWorkDir ,
100
- launcher , "actual" );
101
-
102
- TKit .trace (String .format (
103
- "Check icon file [%s] of %s launcher is a copy of source icon file [%s]" ,
104
- extractedActualIcon ,
105
- Optional .ofNullable (launcherName ).orElse ("main" ),
106
- extractedExpectedIcon ));
107
-
108
- if (Files .mismatch (extractedExpectedIcon , extractedActualIcon )
109
- != -1 ) {
110
- // On Windows11 .NET API extracting icons from executables
111
- // produce slightly different output for the same icon.
112
- // To workaround it, compare pixels of images and if the
113
- // number of off pixels is below a threshold, assume
114
- // equality.
115
- BufferedImage expectedImg = ImageIO .read (
116
- extractedExpectedIcon .toFile ());
117
- BufferedImage actualImg = ImageIO .read (
118
- extractedActualIcon .toFile ());
119
-
120
- int w = expectedImg .getWidth ();
121
- int h = expectedImg .getHeight ();
122
-
123
- TKit .assertEquals (w , actualImg .getWidth (),
124
- "Check expected and actual icons have the same width" );
125
- TKit .assertEquals (h , actualImg .getHeight (),
126
- "Check expected and actual icons have the same height" );
127
-
128
- int diffPixelCount = 0 ;
129
-
130
- for (int i = 0 ; i != w ; ++i ) {
131
- for (int j = 0 ; j != h ; ++j ) {
132
- int expectedRGB = expectedImg .getRGB (i , j );
133
- int actualRGB = actualImg .getRGB (i , j );
134
-
135
- if (expectedRGB != actualRGB ) {
136
- TKit .trace (String .format (
137
- "Images mismatch at [%d, %d] pixel" , i ,
138
- j ));
139
- diffPixelCount ++;
140
- }
141
- }
142
- }
143
-
144
- double threshold = 0.1 ;
145
- TKit .assertTrue (((double ) diffPixelCount ) / (w * h )
146
- < threshold ,
147
- String .format (
148
- "Check the number of mismatched pixels [%d] of [%d] is < [%f] threshold" ,
149
- diffPixelCount , (w * h ), threshold ));
150
- }
151
- });
152
- }
153
-
154
- private WinIconVerifier () {
155
- try {
156
- executableRebranderClass = Class .forName (
157
- "jdk.jpackage.internal.ExecutableRebrander" );
158
-
159
- lockResource = executableRebranderClass .getDeclaredMethod (
160
- "lockResource" , String .class );
161
- // Note: this reflection call requires
162
- // --add-opens jdk.jpackage/jdk.jpackage.internal=ALL-UNNAMED
163
- lockResource .setAccessible (true );
164
-
165
- unlockResource = executableRebranderClass .getDeclaredMethod (
166
- "unlockResource" , long .class );
167
- unlockResource .setAccessible (true );
168
-
169
- iconSwap = executableRebranderClass .getDeclaredMethod (
170
- "iconSwap" , long .class , String .class );
171
- iconSwap .setAccessible (true );
172
- } catch (ClassNotFoundException | NoSuchMethodException
173
- | SecurityException ex ) {
174
- throw rethrowUnchecked (ex );
175
- }
176
- }
177
-
178
- private Path extractIconFromExecutable (Path outputDir , Path executable ,
179
- String label ) {
180
- // Run .NET code to extract icon from the given executable.
181
- // ExtractAssociatedIcon() will succeed even if the target file
182
- // is locked (by an antivirus). It will output a default icon
183
- // in case of error. To prevent this "fail safe" behavior we try
184
- // lock the target file with Open() call. If the attempt
185
- // fails ExtractAssociatedIcon() is not called and the script exits
186
- // with the exit code that will be trapped
187
- // inside of Executor.executeAndRepeatUntilExitCode() method that
188
- // will keep running the script until it succeeds or the number of
189
- // allowed attempts is exceeded.
190
-
191
- Path extractedIcon = outputDir .resolve (label + ".bmp" );
192
- String script = String .join (";" ,
193
- String .format (
194
- "try { [System.io.File]::Open('%s', 'Open', 'Read', 'None') } catch { exit 100 }" ,
195
- executable .toAbsolutePath ().normalize ()),
196
- "[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')" ,
197
- String .format (
198
- "[System.Drawing.Icon]::ExtractAssociatedIcon('%s').ToBitmap().Save('%s', [System.Drawing.Imaging.ImageFormat]::Bmp)" ,
199
- executable .toAbsolutePath ().normalize (),
200
- extractedIcon .toAbsolutePath ().normalize ()));
201
-
202
- Executor .of ("powershell" , "-NoLogo" , "-NoProfile" , "-Command" ,
203
- script ).executeAndRepeatUntilExitCode (0 , 5 , 10 );
204
-
205
- return extractedIcon ;
206
- }
207
-
208
- private Path getDefaultAppLauncher (boolean noIcon ) {
209
- // Create app image with the sole purpose to get the default app launcher
210
- Path defaultAppOutputDir = TKit .workDir ().resolve (String .format (
211
- "out-%d" , ProcessHandle .current ().pid ()));
212
- JPackageCommand cmd = JPackageCommand .helloAppImage ().setFakeRuntime ().setArgumentValue (
213
- "--dest" , defaultAppOutputDir );
214
-
215
- String launcherName ;
216
- if (noIcon ) {
217
- launcherName = "no-icon" ;
218
- new AdditionalLauncher (launcherName ).setNoIcon ().applyTo (cmd );
219
- } else {
220
- launcherName = null ;
221
- }
222
-
223
- if (!Files .isExecutable (cmd .appLauncherPath (launcherName ))) {
224
- cmd .execute ();
225
- }
226
- return cmd .appLauncherPath (launcherName );
227
- }
228
-
229
- private void setIcon (Path iconPath , Path launcherPath ) {
230
- TKit .trace (String .format ("Set icon of [%s] launcher to [%s] file" ,
231
- launcherPath , iconPath ));
232
- try {
233
- launcherPath .toFile ().setWritable (true , true );
234
- try {
235
- long lock = 0 ;
236
- try {
237
- lock = (Long ) lockResource .invoke (null , new Object []{
238
- launcherPath .toAbsolutePath ().normalize ().toString ()});
239
- if (lock == 0 ) {
240
- throw new RuntimeException (String .format (
241
- "Failed to lock [%s] executable" ,
242
- launcherPath ));
243
- }
244
- var exitCode = (Integer ) iconSwap .invoke (null , new Object []{
245
- lock ,
246
- iconPath .toAbsolutePath ().normalize ().toString ()});
247
- if (exitCode != 0 ) {
248
- throw new RuntimeException (String .format (
249
- "Failed to swap icon of [%s] executable" ,
250
- launcherPath ));
251
- }
252
- } finally {
253
- if (lock != 0 ) {
254
- unlockResource .invoke (null , new Object []{lock });
255
- }
256
- }
257
- } catch (IllegalAccessException | InvocationTargetException ex ) {
258
- throw rethrowUnchecked (ex );
259
- }
260
- } finally {
261
- launcherPath .toFile ().setWritable (false , true );
262
- }
263
- }
264
-
265
- static final WinIconVerifier instance = new WinIconVerifier ();
266
-
267
- private final Class <?> executableRebranderClass ;
268
- private final Method lockResource ;
269
- private final Method unlockResource ;
270
- private final Method iconSwap ;
271
- }
272
-
273
80
private String launcherName ;
274
81
private Path expectedIcon ;
275
82
private boolean expectedDefault ;
0 commit comments