пятница, 3 октября 2014 г.

[prog.memories] Совсем древняя часть истории MxxRu

После недавнего релиза MxxRu 1.6.3 решил вспомнить, с чего же все начиналось и как это когда-то развивалось. Оказалось, что многих вещей я уже и сам не помню. И если бы записал это в свое время, то мог бы и не вспомнить. Под катом рассказ о том, как вообще я дошел до жизни такой :) Прошу учитывать, что основные события развивались с 1995 по 2001, тогда нормального доступа к Интернету у меня не было, многие вещи приходилось изучать по очень отрывочным сведениям, а что-то тупо переизобретать, потому что узнать о существовании аналогов было неоткуда, не было тогда ни Google, ни глагола "погуглить" :)

Оригинал заметки лежит на моей старой страничке на narod.ru, там же есть и подробная документация по mxx4.

Необходимость средства, подобного нынешнему M++, возникла где-то в 1995 году. Тогда для разработки некоего проекта под условным названием ot2 применялся Borland C++ v.3 (компилятор и IDE). Но появился компилятор Borland C++ v.4.0, а затем Borland C++ v.4.5. Проекты из IDE v.3 в IDE v.4 просто так не открывались. Кроме того, хотелось поддерживать возможность компиляции проекта как компилятором v.3, так и компилятором v.4. Для этого приходилось поддерживать две версии проектного файла для каждого модуля (DLL, статических библиотек, EXE-файлов). А модулей насчитывалось порядка 20 и это количество постоянно увеличивалось.

Кроме того, в IDE не представлялось возможным сразу перекомпилировать несколько модулей, если изменения были внесены в модуль, который остальные модули используют. Для этого приходилось последовательно открывать различные проектные файлы и перекомпилировать модули по одному.

Уже тогда я считал, что программы должны быть максимально переносимыми. Пусть не на различные операционные системы, так хоть между различными компиляторами. Поэтому, когда в мое распоряжение попал компилятор Watcom C++ v.10, мне захотелось, чтобы ot2 мог компилироваться и им тоже (к счастью, в проекте использовался только чистый WinAPI и отказ от какого-то компилятора не влек за собой необходимость перехода на новую оконную библиотеку). К этому времени я пытался использовать make-файлы и мне казалось, что с их помощью адаптация проекта к новому компилятору не вызовет особой сложности в части организации компиляции и линковки модулей проекта.

Но и в использовании make-файлов были свои сложности. Лично я всегда забывал, как просредством макросов make указывать цель, как зависимости. Т.е. на момент написания конкретного make-файла я помнил, что означает $<, $@ и т.д. Но спустя неделю-другую я напрочь об этом забывал. И когда возникала необходимость написать make-файл для нового модуля снова приходилось искать описание встроенных макросов make. И так снова и снова.

Отдельной проблемой являлось указание одинаковых опций компилятора для всех модулей проекта. Если указывать опции непосредственно в каждом из make-файлов, то при их изменении нужно поправить все make-файлы. Более простым решением являлось хранение опций в отдельном внешнем файле, который бы подключался посредством директивы include в каждом make-файле.

Оказалось, что применение make-файлов гораздо удобнее, чем проектных файлов конкретной IDE, хотя и сопряжено с некоторыми трудностями. Но все это хорошо, пока проект разрабатывается средствами одного компилятора. Знакомство со средствами трансляции и линковки модулей компилятором Watcom C++, показало, что для каждого компилятора нужно поддерживать собственный набор make-файлов, ориентированных на конкретный инструмент и вариант утилиты make.

Мало того, что сам make называется в разных компиляторах по разному (make в Borland, wmake в Watcom, nmake в Visual C++), но и правила оформления make-файлов так же различаются (и функциональность каждого make так же разная). Например, в make и nmake при необходимости продолжения перечисления файлов на новой строке в конце строки необходимо было поставить символ \, а в wmake - символ &. Различные версии make имели собственный препроцессор (так директива include могла записываться либо !include, либо $include, либо @include). wmake предоставлял операцию += для заполнения значений макросов и возможность доступа к отдельным элементам списка зависимостей, а другие make - нет. make от Borland позволял создавать response-файлы специальными конструкциями внутри make-файла. Для других make response-файлы нужно было создавать выдавая последовательности комант echo. А уж оформление условий в препроцессорах каждого make были совершенно различны как по синтаксису, так и по своим возможностям.

К этому нужно добавить то, что каждый компилятор имеет не только различный набор опций, но и различные правила их указания. Так, правила задания опций линкеру tlink совершенно отличаются от правил для wlink или link.

Т.е. получалось, что для каждого компилятора нужно поддерживать собственный набор make-файлов. Соответственно при внесении изменений в какой-либо модуль необходимо было изменить все его make-файлы (что иногда либо не делалось, либо делалось с ошибками). А при создании нового модуля необходимо было создать для него несколько make-файлов, для чего нужно было восстановить в памяти особенности каждого make.

И все это при том, что каких-то особых ухищрений для модулей не требовалось. Нужно было только указать, что из себя представляет модуль - DLL, статическую библиотеку или EXE-файл, является ли модуль оконным или консольным приложением и перечислить список исходных C++ файлов и необходимых библиотек.

Поэтому, когда мне в очередной раз надоело вспоминать, как именно в компиляторе Borland указать сборку EXE-файла для консольного режима (хотя, может быть, это было что-то другое) я решил, что нужно придумать такой формат описания проекта, в котором бы нужно было просто указать что и из чего нужно получить. А затем какой-то инструмент сам должен разобраться какой компилятор использовать. В качестве такого инструмента я решил применить wmake, т.к. в нем были наиболее развитые, на мой взгляд, средства препроцессора.

Так на свет появился Make++ v.1, который представлял из себя просто набор make-файлов для wmake. Один из них нужно было подключить в собственный файл проекта и запустить wmake, сообщив в командной строке тип компилятора (Borland или Watcom). При этом файл-проект выглядел как набор деклараций без явного описания make-правил. Это позволило не только быстро менять компилятор при сборке ot2, но и применить mpp в моем собственом проекте, в котором нужно было еще и поддерживать операционные системы Windows и OS/2.

Но, у Make++ v.1 было два очень важных недостатка. Во-первых, препроцессор и правила написания make-файлов для wmake не поддерживали структурного программирования. Поэтому make-файлы, предназначенные для перевода файла-проекта в набор обычных make-правил были написаны так сложно и запутано, что разобраться в них было черезвычайно трудно. Во-вторых, Make++ v.1 работал только там, где был wmake. Т.е. в Windows и OS/2. А мне уже хотелось попробовать перебраться под UNIX (в лице Linux и Solaris).

Поэтому я написал автономный вариант M++ v.2 на C++. M++ v.2 был самостоятельной программой, которая расшифровывала проектные файлы (формат которых изменился, но остался очень похожим на формат проектов Make++ v.1), а затем генерировала make-файлы и запускала make. При этом она учитывала особенности конкретного семейства компиляторов. Кроме того, в M++ v.2 я реализовал т.н. анализатор C++ зависимостей - путем простого просмотра C++ файлов определялось, какие заголовочные файлы использовались. Далее следовала попытка просмотреть выявленные заголовочные файлы и т.д. В результате в генерируемый make-файл содержал для каждого C++ файла список заголовочных файлов, от которых зависит C++. А это значительно облегчало разработку, т.к. изменение какого-либо из заголовочных файлов автоматически приводило к перекомпиляции и перелинковке всех модулей, использующих измененный заголовочный файл.

В целом M++ v.2 оказался полезным инструментом. Написан он был в очень короткий срок (что-то около недели), а поддерживал несколько компиляторов (Borland C++ под Win, OS/2; Watcom C++ под Win, OS/2; Visual C++; GNU C++ под Linux, Solaris; затем и IBM Visual Age C++ под OS/2). Но его развитие и сопровождение оказалось слишком трудоемким, поскольку внесение любых изменений требовало программирования на C++. При этом, M++ v.2 не был make в чистом виде, он просто переводил жестко зафиксированные в нем наборы правил в make-файл конкретного компилятора. И добавление новой возможности означало перепрограммирование средств генерации make-файлов, затем компиляции и отладки M++ v.2. Почему-то внесение изменений в M++ v.2 (например, исправление обнаруженной ошибки) оказывалось необходим на компьютере, на котором вообще не было исходных текстов M++ v.2. Да и сам формат описаний был слишком громоздким и не красивым.

Все это, вместе с тайным желанием создать собственный язык программирования, привело к появлению M++ v.3. C M++ v.2 его объединяло только то, что в проектных файлах нужно было заполнить заранее определенные списки значений. В остальном M++ v.3 являлся совершенно другим инструментом. Во-первых, он не нуждался в make, т.к. сам был таковым. Во-вторых, M++ v.3 являлся интерпритатором собственного специализированного языка. И все правила преобразования файлов проектов в набор make-правил были описаны на самом M++ v.3. А это позволяло очень быстро и просто вводить новые правила, исправлять ошибки, настраивать M++ v.3 на новый компилятор и/или операционную систему.

Не смотря на то, что M++ v.3 был разработан в достаточно короткий срок (порядка 2-3 недель) это оказался очень удобный инструмент. Гораздо более удобный, чем M++ v.2. Но и в нем со временем обнаружился недостаток, который был заложен еще на этапе проектирования языка. Дело в том, что командный язык был сильно специализированым и наращивание M++ v.3 новыми возможностями, не заложенными в него изначально (например, получения списка файлов в каталоге), требовало изменения самого языка. Т.е. необходимо было менять грамматику языка, дополнять лексический и семантический анализатор, что было не удобно и, главное, усложняло сам язык.

Поэтому, я решил создать новую версию M++, так же опирающуюся на собственный язык. Но этот язык не должен был быть привязан к конкретным возможностям самого M++. Вместо этого все возможности M++ оформлялись в виде некоего API - набора функций. И программирование средств управления проектами сводилось бы к обращению к M++ API. Расширение же функциональных возможностей M++ осуществлялось бы за счет расширения набора API, а не за счет изменения языка.

Так появился M++ v.4. Формат файлов оказался не совместимым с форматом файлов M++ v.3. Но идеалогия создания проектов и правил преобразования проектного файла в набор make-правил осталась практически неизменной. В результате все средства поддержки проектов в M++ v.4 были получены путем небольших изменений аналогичных средств из M++ v.3.

Сегодня кажется, что M++ v.4 оправдывает возложенные на него ожидания. Да и весь подход, заложенный в способ формирования проектных файлов в семействе средств M++ получился настолько удачным, что сам M++ v.4 компилируется с помощью M++ v.3.

Ну и для примера маленький фрагмент кода на mxx4:

#if !defined( __FINISH_4XX )
#define __FINISH_4XX

#include <postload.4xx>

if( dependOnPrjs ) {
   #if defined( MXX4_VERBOSE )
      io_print( "--- Start make sub-projects for target : " +
         target + " ---\n" );
   #endif

   foreach( p in dependOnPrjs ) {
      string cmd_line;
      string error_code;

      cmd_line = "mxxc -f " + p + " " +
         str_array_to_str( mxxc_query_cmd_line_args() );

      #if defined( MXX4_VERBOSE )
         io_print( "*\n*\t" + p + "\n*\t" + cmd_line + "\n*\n" );
      #endif

      if"0" != ( error_code = sys_run( cmd_line ) ) ) {
         mxxc_set_exit_code( error_code );
         halt;
      }
   }

   #if defined( MXX4_VERBOSE )
      io_print( "--- Finish make sub-projects for target : " +
         target + " ---\n" );
   #endif
}


#if defined( URCSTAGS )
#include <urcstags/finish.4xx>
#endif

#include <make.4xx>

#endif
Отправить комментарий