@@ -77,199 +77,6 @@ public void applyTo(JPackageCommand cmd) throws IOException {
7777 }
7878 }
7979
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-
27380 private String launcherName ;
27481 private Path expectedIcon ;
27582 private boolean expectedDefault ;
0 commit comments