Thursday, August 29, 2013

Another word on using RPM Maven Plugin under Windows

Hi everyone.

There've been several posts covering same topic: one, two and even a bug in plugin's JIRA.

Shortly, the whole idea behind building RPMs under Windows states as follows.
The plugin relies on the existence of utility called rpmbuild (which should be accessible from cmd).
This utility is usually pre-installed with most Linux distributions which support rpms. After adding corresponding packages to cygwin, it becomes available under Windows: though not as a separate executable, but rather under bash command line.

What one may do under Windows is cheating the plugin: we create some file called rpmbuild.bat (under a directory added to the PATH environment variable). So the plugin (when executing rpmbuild in cmd) starts thinking that it is feeding its parameters to real rpmbuild, but under the hoods (inside the batch-file) we redirect all of the parametrs to real rpmbuild in bash cmd.
However there is an issue with this approach. The plugin inserts Windows-style paths: both into cmd and into SPEC-file (required to build rpm).
rpmbuild.bat -bb --target "noarch-pc-windows 7" --buildroot D:\MyPath\MyApp\buildroot --define "_topdir D:\MyPath\MyApp" MyApp.spec
However cygwin is not friendly with windows paths. What is required looks like:
bash -c "rpmbuild -bb --target ""noarch-pc-windows 7"" --buildroot /cygdrive/d/MyPath/MyApp/buildroot --define ""_topdir /cygdrive/d/MyPath/MyApp"" MyApp.spec"
In the references I provided in the beginning of this post, people suggest fixing both issues and provide a batch-script. However that batch-script didn't work for me: the order of parameters passed to rpmbuild.bat appeared to be different. Probably it is related to another version of RPM Maven Plugin (I'm using 2.1-alpha-3).

Firstly I just modified the numbers in parameters of their scripts (precisely, the order of parameters) and everything went smoothly. However I didn't like this solution because it is not robust.

I decided to extend their solution to a more general case: when we don't know how many parameters have been passed and their order, we only have some conditions to check (against the parameters list) and corresponding parameters to fix.
The idea of my script states as follows.
Enumerate all parameters one-by-one. If previous parameter == "--buildroot", then current parameter must be path and we transform it to cygwin path (via cygpath utility). If current parameter starts with "_topdir, then we can extract path and convert it to cygwin path. If current parameter ends with .spec, then it's likely the SPEC-file, we should replace paths inside it with sed utility.

To store source parameters (passed to bat-file), I decided to create an array: PARAMS[1..N]. Actually it's not a real array in terms of batch-scripting, but syntactically similar to usual arrays we have in programming languages. I save each and every source parameter in this array, while later modifying some of them according to the described rules.
I ended up with the following script:

SETLOCAL EnableExtensions EnableDelayedExpansion PUSHD . SET PARAM_COUNT=0 FOR %%A IN (%*) DO ( SET /A PARAM_COUNT+=1 SET PARAMS[!PARAM_COUNT!]=%%A IF !PARAM_COUNT! GTR 1 ( SET /A BEFORE_LAST = !PARAM_COUNT!-1 CALL SET "BEFORE_LAST_PARAM=%%PARAMS[!BEFORE_LAST!]%%" IF "!BEFORE_LAST_PARAM!"=="--buildroot" ( REM Update buildroot path with cygwin path FOR /F "tokens=*" %%i in ('cygpath %%A') do SET PARAMS[!PARAM_COUNT!]=%%i ) ) REM check if string starts with "_topdir SET str1=%%A SET START_STR="_topdir IF "!str1:~0,8!"=="!START_STR!" ( REM Update topdir path SET TOPDIR=%%A SET TOPDIR=!TOPDIR:~9,-1! FOR /F "tokens=*" %%i in ('cygpath "!TOPDIR!"') do SET NEW_TOPDIR=%%i SET PARAMS[!PARAM_COUNT!]="_topdir !NEW_TOPDIR!" ) REM check if string ends with .spec SET END_STR=.spec IF "!str1:~-5!"=="!END_STR!" ( REM Replace path in spec-file SET OLD_PATH=!TOPDIR:\=\\! SET NEW_PATH=!NEW_TOPDIR:/=\/! sed -s -i -e s/!OLD_PATH!\\/!NEW_PATH!\//g %%A REM replace all backslashes with slashes in lines like %attr  "/path1/path2/path3\path4\somefile.ext" REM original command should be: awk -F '"' '/^%attr/{gsub(/\\/, "/", $(NF-1))} 1' OFS='"' %%A > _tmp && mv _tmp %%A bash -c "awk -F '""' '/^%%attr/ {gsub(/\\\\/,""/"", $(NF-1))} 1' OFS='""' %%A > _tmp && mv _tmp %%A" ) REM check if source param was quoted, then double the double-quote SET QUOTE=" IF "!str1:~0,1!"=="!QUOTE!" ( CALL SET "LAST_PARAM=%%PARAMS[!PARAM_COUNT!]%%" SET PARAMS[!PARAM_COUNT!]="!LAST_PARAM!" ) ) REM construct new rpmbuild command in cygwin SET RPMBUILD_COMMAND=bash -c "rpmbuild FOR /L %%i IN (1,1,!PARAM_COUNT!) DO SET RPMBUILD_COMMAND=!RPMBUILD_COMMAND! !PARAMS[%%i]! SET RPMBUILD_COMMAND=!RPMBUILD_COMMAND!" REM Execute rpmbuild ECHO !RPMBUILD_COMMAND! !RPMBUILD_COMMAND! POPD ENDLOCAL

As one might notice, despite the fact other authors do the following, I don't add
--define ""_build_name_fmt %%{ARCH}/%%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm""
This is not to over-complicate this script. Any required define-statement can be easily specified in plugin configuration:
<plugin>
  ...
  <configuration>
    ...
    <defineStatements>
      <defineStatement>_build_name_fmt %%{ARCH}/%%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm</defineStatement>
    </defineStatements>
    ...
  </configuration>
  ...
</plugin>
In the above script you may observe some difficulties in working with such "arrays". For an element before last you can't simply write smth like !PARAMS[!PARAM_COUNT!-1]!, because the arithmetic result won't get substituted, moreover, even !PARAMS[!PARAM_COUNT!]! is not allowed, because it's smth like Delayed Expansion inside Delayed Expansion. As a workaround I used CALL SET "blah..blah"-construction, introducing an interim variable (you can notice it twice in the above script). Credits go to Endoro on stackoverflow in this question.

To end with, this script may also fail in some sophisticated (or not really :) cases, but at least it may serve as a good starting point for improvements.

UPD (Oct 11, 2013). In SPEC-file some lines (starting with %attr) still had windows-style paths with backslashes, I replaced them with slashes through these line(s):
REM replace all backslashes with slashes in lines like %attr  "/path1/path2/path3\path4\somefile.ext" REM original command should be: awk -F '"' '/^%attr/{gsub(/\\/, "/", $(NF-1))} 1' OFS='"' %%A > _tmp && mv _tmp %%A bash -c "awk -F '""' '/^%%attr/ {gsub(/\\\\/,""/"", $(NF-1))} 1' OFS='""' %%A > _tmp && mv _tmp %%A"
For details refer to: http://stackoverflow.com/questions/19313825/