...

суббота, 4 мая 2019 г.

[Из песочницы] Как я стал PMP и как это лучше не делать

Я давно осознал, что делиться опытом — это полезно. Но только недавно я понял, что им можно делиться не только со знакомыми и близкими, но и со всеми.

Поэтому, я хочу рассказать, как я шел к сертификации, почему я это делал и чем это закончилось.
Мой опыт будет разделен по разделам:

  1. Почему PMP?
  2. Подготовка — планирование
  3. Начало подготовки
  4. Аудиты
  5. Завершение подготовки
  6. Экзамен
  7. Жизнь после

Так как я люблю целостные рассказы и предысторию, то первый раздел можно не читать тем, кто торопиться.
Я узнал про PMI достаточно давно — до момента сертификации произошло более 7 лет. И мое первое знакомство произошло в 2011 году, когда товарищ рассказал мне про PMBoK во время простой прогулки. Мы тогда не обсуждали работу или дела — просто болтали обо всем подряд.

Так как я достаточно любопытный человек, то я стараюсь узнавать новое при первой возможности (и даже бесполезное на первый взгляд). Поэтому, придя домой я скачал 4 редакцию свода на русском и достаточно быстро его прочитал.

Как бы я прокомментировал тогда эту книгу? Замудренная, местами непонятная, местами чересчур очевидная и тяжелая для восприятия.

А в 2012 году я столкнулся с явными проектами, придя на новую работу. Я говорю с явными — потому что я сталкивался с ними и ранее, но не осознавал этого. Поэтому, я начинаю свой отсчет опыта в проектном управлении именно с 2012 года.

И несмотря на то, что они были для меня первыми — они были сложными и комплексными, федерального масштаба. Это были проекты по автоматизации деятельности всей структуры федерального органа, а еще — по взаимодействия с регионами.

И руководителем проекта я стал не сразу, да и вообще — становиться не очень хотел. Но вот где я был изначально — это в команде управления проектами.

Все ли делалось и получалось правильно? Абсолютно точно — нет. Но я всегда старался сделать все хорошо. Чаще всего страдали сроки и эффект от внедрения. Не сильно, нет — но все же, что-то доделывалось еще недели две-четыре после закрытия проекта формального.

А самое интересное, что когда я делал все правильно — то я почти всегда вспоминал PMBoK.
Приходил к тем же выводам. Не всегда явно, но все же.

В 2014 году у меня на счету был уже не один проект такого масштаба и я уже был не раз руководителем этих проектов. Не по своей воле — но мне сказали, что это надо сделать и я делал. Не все было супер, но я не могу сказать, что хоть один из проектов был провален (и за это отдельное спасибо всем участникам, от пользователей до подрядчиков). И в это время я стал изучать свод знаний уже более осознанно, и это была 5 редакция.

И именно тогда я узнал про сертификацию. Узнал и захотел стать PMP. Но посчитав и почитав, я понял, что опыта еще недостаточно, а в знаниях я сильно не уверен и отложил вопрос.

В 2015 году я был РП со стороны подрядчика. Это был для меня откровенно другой опыт. Именно тогда я осознал, насколько все бывает не идеально — а под все, я подразумеваю работу людей (ведь именно от них зависит по сути весь проект). Когда в оценках сроков специалисты ошибаются не на проценты, а на порядки, когда тебе обещают ресурс, но по факту — не дают. Да и много чего было. Но несмотря на это — мы справлялись и проекты завершались.

К 2017 году для меня случилось несколько неожиданностей:

  1. Меня не отправили на обучение руководителей проектов от департамента, хотя туда отправили по-моему всех.
  2. Меня отправили к клиенту разрабатывать методологию проектного управления и внедрять ее, несмотря на п.1.
  3. Меня пригласили на международный проект на позицию руководителя проектного офиса, который предстояло создать с нуля.

Работу я сменил и вышло так, что почти полтора года своей жизни я провел в обнимку со сводом, не изучая его, но внедряя разные процессы и документы. Отдельное спасибо Юрию, начальнику отдела нашего клиента, который очень старался получить от проектного офиса больше, чем это было возможности по ряду политических и организационных факторов.

И в 2018 году, когда встал вопрос с планом личного развития на год, в него был включен пункт по получению сертификата PMP. До декабря (включительно) я должен был им стать.


Итак, конец февраля и передо мной поставлена задача. Трачу пару дней на прочтение отзывов о сдаче экзамена, различных программ по подготовке и составляю свой план.

Он был прост и понятен, уточнять я его планировал по факту:

  1. Вступить в PMI
  2. Получить актуальный PMBoK
  3. Заказать Rita Mulcahy PMP exam prep
  4. К концу лета дочитать обе книги (срок ставился с учетом работы и возможных сроков доставки бумажной книги Риты из США)
  5. На сентябрь найти курсы для получения PDU (на тот момент, в связи с путаницей понятий и еще рядом факторов, я не знал, что PDU не обязательны для этого). Как неплохой вариант — виделись курсы в Специалисте
  6. После курсов прочесть Риту еще раз
  7. Определиться с датой экзамена (планировал — конец ноября или начало декабря)
  8. Перечитать Риту еще раз
  9. Сдать экзамен

Не все пошло по плану.
Первые два пункта я выполнил отлично. В свой день рождения оплатил членство в PMI, скачал PMBoK 6 редакции на русском и английском. Пункт № 3 плана несколько уточнился — жена, узнав про Риту и про ее симулятор уговорила заказать полный набор — книга + карточки + симулятор.
О бонусах членства

Вступая в PMI вы получаете право на скачивание материалов и стандартов. Вы получаете эти документы с отметкой, что они принадлежат только вам и с паролем, без права на печать. Однако это — отличная экономия. И да, с точки зрения PMI скачивание материалов со сторонних ресурсов — нарушение кодекса этики.


Риту я заказал через Бандерольку. Почему? Первый посыл был ради экономии, тем более мне не горело. Но когда стал изучать вопрос подробнее — то понял, что даже если бы горело, то заказал бы также. DHL все посылки стоимостью более 200$ проводит через таможню, и там без разницы израсходовали вы свой лимит или нет (я про то, что граждане РФ имеют право заказать товаров на 1000$ в месяц без выплаты налогов. Кстати, теперь этот лимит стал 500$). А формально, несмотря на то, что в посылке нету никакой программы — лицензионный код присылают по почте, в инвойсе значится вся сумма. А это более 300 $.
Экономия не прямой доставки

В итоге, экономия за доставку не сильно сложилась. Разница была порядка 5$, так как я не корректно рассчитал вес посылки. Однако за счет отсутствия налога — я сэкономил примерно 100$, что позднее подтвердили коллеги, заказавшие Риты через DHL полным набором.


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

На обучение я попал и это было круто. Обучение вел VP MO PMI А.А. Зубрицкий. Это было полезно не только в плане подготовки, но и позволило мне пообщаться вживую с PMP, узнать какие-то тонкости (да, живых PMP до того момент я не видел. Для меня это были какие-то легенды). В общем, готовлюсь, читаю, хожу на занятия раз в неделю, обсуждаю в перерывах что-то непонятное.

И тут наступает во-вторых. Мне предложили другую работу. Надо сказать, что к тому моменты я не сильно держался за место по ряду причин. А новый вариант был достаточно интересный для развития и я согласился. Как раз совпало, что уволюсь я к концу курса.

Пока отрабатываю положенный срок, приходит Рита и я начинаю ее читать. Один из первых советов — подавайте заявку как можно раньше. Я прикидываю, соглашаюсь с этим и начинаю искать курсы онлайн — чтобы набрать часы обучения. Без них нельзя подать заявку, а сертификат за корпоративный курс мне не факт что дадут — я же увольняюсь, а если и дадут — то не сразу. И я нахожу Udemy. И там есть курс подготовки к PMP за 9.99$ с начислением PDU от Joseph Phillips. Так вот — он неплох.

О PDU

К тому моменту, я уже знаю, что PDU не нужны и будет достаточно часов любого обучения проектному управлению. В случае отсутствия сертификата после обучения, достаточно приложить программу обучения и письмо (бумажное, электронное) что вас туда пригласили. Если нет письма — договор, заявка.


За полторы недели, еще не уйдя с работы я прослушиваю курс и подаю заявку на PMP.
Наверное, я бы надолго запомнил тот день, если бы дальше все сложилось иначе. 26.04.2018, подав заявку я попал на аудит. И это очень неприятно морально — потому что редко какой отзыв про экзамен содержит информацию про аудит.

Ладно, работаем. Когда я подавал заявку, я достаточно предусмотрительно обсудил ее со всеми контактными лицами. Я им рассказал что указываю их, по каким проектам, и что это такое, опасаясь забывчивости людей или нежелания общаться с незнакомцами — потому что считал, что PMI может проверить без аудита телефонным звонком. Я тогда не думал про аудит, но это пригодилось.

Пригодилось, но как выяснилось — я недоработал. С опытом в коммерции все прошло отлично. Поймал ГД группы компаний, подписал документы, подписали конверт. Так, как на той работе я был на хорошем счету — все прошло быстро и безболезненно.

Опыт с последней работы я даже не фиксировал — мне его хватило и так.

А вот та часть опыта, которая была на госслужбе — стала проблемой. На бланке отзыва указывается адрес PMI — США. А еще там указывается должность лица, подписавшего отзыв. А у нас вообще-то напряженная обстановка, санкции. И те должностные лица (замы управлений), с которыми я это обсуждал — отказывают мне, увидев адрес.

Об ошибках

Я много думал, как так вышло, что я все проговорил, предупредил и мне отказали пост-фактум, изначально выразив согласие. Я же говорил, что штаб организации в США. И пришел к выводу, что виноват сам. Потому что я также говорил, что организация международная и у нее есть офис в Москве.


Пару дней я паниковал. Паниковал настолько, что уже начал договариваться о встрече с первыми лицами службы, чтобы они или подписали отзывы сами или дали согласие тем сотрудникам, с которыми я это обсуждал.

А потом я вспомнил, что опыт может подтвердить также сотрудник не из проекта, член команды или даже исполнитель. Еще неделя ушла на получение всех подписей.

Собирая документы, я написал сам перевод диплома, а также объяснительную, почему отзыв дали не те люди, которые указаны в заявке.

Об оформлении документов на аудит
Помните, что документы по каждому проекту, или же по каждому подтверждающему ваш опыт должны быть запечатаны в отдельные конверты, с подписью этих лиц на конверте. Это делается для того, чтобы исключить фальсификацию.

А еще — люди могут отправить документы напрямую в PMI. Но это сложный путь.


Итак, документы отправлены, а я продолжаю готовиться. И эта подготовка идет сильно тяжелее. Читать Риту не удобно. Не удобно по бытовым причинам — это здоровая книга формата А4, которую сложно читать в метро, сложно читать в парке или на работе. Поэтому, у меня уходило по неделе или двум на главу. И только дома — по выходным.

17 мая я по треку вижу, что письмо получено. Тут же пишу в комиссию, переживая, на что получаю ответ — документы получены.

И в тот же день получаю письмо о том, что аудит провален.

В тот день я не дочитал письмо до конца, настолько был расстроен. Я был очень расстроен. Несмотря на смену работы — я хотел сертифицироваться, просто для себя, чтобы знать — хорош ли я.

И вот, на следующий день я дочитал письмо и настроение поднялось. Да, аудит провален, но провален по техническим причинам и без вины кандидата. А это значит, что у меня нету штрафа на год. Просто из документов не ясна моя роль и не ясны проекты.

И я с этим согласен — я старался вместить в ограниченное количество символов максимум деталей. А сложно описать проекты такого уровня в таком маленьком объеме знаков. А еще кейсы с госслужбы я дробил не совсем корректно — иногда запихивая в один проект годовой объем работы (сказалось общение с Минкомсвязью).

До 22 мая я активно переписываюсь с PMI, уточняя формулировки, чтобы по ним не было вопросов. Попутно я предупреждаю всех, что снова буду просить их подпись. И 22.05.2018 я снова подаю заявку, тут же попадая на аудит.

О заявке

Важно описывая свои проекты — описывать их максимально формализовано с точки зрения стандарта и терминов PMBoK. Иначе вас могут не понять.


Итак, документы быстро собраны и отправлены и мне остается только ждать. Май на новой работе был спокоен, но вот в июне стало жарко. Поэтому, когда 19.06.2018 приходит ответ, что я прошел аудит — я не сильно радуюсь. Просто воспринимаю это как должное и тут же оплачиваю экзамен. У меня есть ровно год.

Но все идет не по плану.


Новая работа, особенно, когда там много с нуля требует очень много времени. Поэтому к концу ноября моя ситуация по подготовке выглядит не очень хорошо. Прочитала только половина Риты. Я очень надеюсь на декабрь (тут у меня нет привязки к тому, что это конец года) и на праздники.
И в декабре я попадаю в больницу. Выпускают меня под новый год, после операции. В конце января — надо снова лечь. И увы, в таком состоянии мне не до подготовки. Все праздники я прихожу в себя, а потом — мне тяжело просто тянуть работу, не то, чтобы готовиться.

В середине февраля я окончательно прихожу в себя (ну почти, по мелочам — до сих пор отхожу). И я вспоминаю про экзамен. Решив не тянуть, 19.02 я подаю заявку на 23.03.2019.

Почему такой срок? На то есть несколько причин:

  • Надо же прочитать Риту до конца.
  • Симулятор Риты оплачен до 28.03.
  • Членство в PMI оплачено до 31.03.

И или я сдам — и тогда я PMP, я это отмечу и порадую себя, или я просто не буду продлевать членство, удалюсь из чатов и вернусь к этому вопросу через пару лет. Или не вернусь.

И за первую неделю я прохожу 2 главы Риты, а еще открываю режим Super-PMP в симуляторе, пройдя 150 вопросов. Но это мой единственный явный успех. Потому что потом накатывает работа — все же почти квартал работы в половину силы сказывается на объеме того, что еще надо сделать.

На дату 16 марта у меня оставалось не прочитано еще 3 главы Риты. А еще я почти не решал тесты с того момента, как открыл Super-PMP. Поэтому, неделя ударная. Я трачу все свободное время на чтение, симулятор, а также приложения. Кстати, большая часть приложений откровенно взяла вопросы из симулятора Риты. За неделю я отправляю 2 письма разработчикам приложений — про ошибки. А еще я дочитываю Риту и в симуляторе я прорешал около 700 вопросов.

Итак, 22 марта, вечер пятницы. Рита прочитана один раз, результаты моих тестов — от 60 до 75%. Несмотря на просьбу жены выспаться — сижу до ночи, пролистывая Риту и вспоминая все (хотя бы потому, что начало я читал почти год назад).

Итого по плану — он почти провален. Рита не прочитана трижды. Показатели в симуляторе ниже рекомендуемых. Я совсем не уверен в ситуационных вопросах, но я уверен в математике и критическом пути. Немного сомневаюсь в ITTO’s.

И в час ночи я ложусь.


Конечно, раз пошла такая подготовка, то и рекомендации выспаться я не мог не нарушить. Я просыпаюсь в 4 час от стука на весь дом. То ли кто-то чистил мусоропровод, то ли выносил очень тяжелую мебель. Это длилось больше часа. Поняв, что не усну — я стал играть в телефон.

В 8.50 я вышел из метро и начал искать Prometric. Это вышло быстро. В 9.10 я получил пропуск, а в 9.15 уже слушал про экзамен и проходил досмотр. В 9.30 я уже за компьютером. Всего в этот день на PMP сдавало 3 человека (хотя заявлено было 4). А всего в аудитории было то ли 5, то ли 6 человек.

Выдох и я решаю не писать себе подсказок. То, что я знаю — я знаю и так. А что не знаю — мне не поможет, ведь я не выпишу этого заранее. Трачу 3 минуты на обучение программе — трачу их на то, чтобы проверить как работает выделение, а также отметки вопросов.

И пошли вопросы. Было ли легко? Нет. Совсем нет.

Первое, что хотел бы отметить — русский больше мешает, чем помогает. Такое ощущение, что перевод машинный, притом по алгоритмам устаревшим еще лет 10 назад.

О переводе

Те, кто стал PMP не зная английский — вы герои. Я не считаю, что это правильно, сдавать международный сертификат, не зная язык, но это достижение. Я перестал смотреть на перевод на 5 вопросе. Он мне пригодился только один раз, ближе к концу — когда я споткнулся на сложной фразе.


Во-вторых — вопросы не похожи на приложения и симулятор! Совсем! Они не проще, не сложнее, они другие! И мне кажется, это меня и спасло. То ли стресс стимулировал осознание, то ли вопросы были “правильные”, но на 10 вопросе я осознаю, что работа мозга перестроилась. То, о чем раньше мне надо было подумать — сейчас осознается само. Я просто мыслю в формате PMBoKa, а не вспоминаю — как бы было лучше.

Симулятор такого эффекта не вызывал. Я допускаю, что это был просто качественный скачок, но не уверен.

Ну и третье. Мне не повезло. У меня было всего 3 вопроса на формулы и 2 или 3 на критический путь. С учетом того, что я рассчитывал на 20-40 таких вопросов и был в них уверен — это меня не обрадовало.

К концу первого часа я прорешал 90 вопросов. Сомневался и отметил 5 или 6. Добив до 100, я сделал паузу.

У меня начала болеть голова. Делаю разминку шеи, даю отдохнуть глазам и думаю, как быть. Даже если бы в вещах были таблетки — вещи на замке. А я знаю себя, если я не выпью таблетку, то станет очень плохо. И я решаю, что я закончу как можно быстрее. Я уверен, что я или отвечу с первого раза или не отвечу вообще на вопросы, поэтому — никаких проверок.

Последние 20 или 25 вопросов я даже не дочитывал варианты ответов. Если я видел, что первый из них хорош, я отмечал его. И вообще, на последние 30 вопросов у меня ушло менее 10 минут. У меня очень болела голова.

Я быстро прошел опрос от Prometric и вышел.

— Решили отдохнуть? — уточнила сотрудница центра.
— Нет, я все — отдал ей бланк и начал доставать вещи.
— Быстро вы — проверив компьютер, что я на самом деле закончил, она поставила отметку на бланке и стала печатать результат.

На вопрос, есть ли таблетки от головы — мне ответили отрицательно.
В 11.45 из коридора жене было отправлено фото бумажного отчета об экзамена.
PASS. Above target.

На весь экзамен ушло чуть больше 2 часов.


В 11.50 я уже купил таблетку в аптеке на 1 этаже здания. В метро голову отпустило. И только когда отпустило, я понял, что все закончилось хорошо. Что несмотря ни на что — я все же хороший специалист.

Из метро я продлил членство в PMI. Пригласил жену в ресторан. И в тот же день я купил себе хорошую перьевую ручку.

Я абсолютно уверен, что совет — отпраздновать — верный. Таким моменты должны быть в памяти. Это круче дня рождения или нового года.

Спустя 2 дня я появился в реестре и на почту пришло письмо с электронным сертификатом. Тут же заказал себе значок, он пришел через 5 недель. Сертификат — нет.

Пошел ли я просить повышения зарплаты на работе? Нет. Я это делал для себя и для подтверждения своих компетенций и навыков. Пусть я в себе много сомневаюсь, но я профессионал.
Я PMP.
Принесло ли мне это результат? Определенно да.
Да и морально и социально.
Морально — я стал более уверен в себе.
Социально — я осознал себя частью сообщества, которое старается делать правильные вещи. И да, это расширило круг знакомств, даже спустя месяц стали появляться новые активности. А еще — среди тех, кто знает и понимает — это реально ценится.

И это стоит того.

Let's block ads! (Why?)

Что слышно в радиоэфире? Принимаем и декодируем наиболее интересные сигналы

Привет Хабр.

На дворе уже 21й век, и казалось бы, передать данные можно в HD-качестве даже на Марс. Однако, до сих пор в радиоэфире работает немало интересных устройств и можно услышать немало интересных сигналов.

Все из них рассмотреть разумеется, нереально, попробуем выбрать самые интересные, те которые можно принять и декодировать самостоятельно с помощью компьютера. Для приема сигналов мы воспользуемся голландским онлайн-приемником WebSDR, декодером MultiPSK и программой Virtual Audio Cable.

Для удобства рассмотрения будем приводить сигналы по возрастанию частоты. Вещательные станции я рассматривать не буду, это скучно и банально, послушать Радио Китая в АМ желающие могут и самостоятельно. А мы перейдем к более интересным сигналам.

Сигналы точного времени


На частоте 77.5КГц (диапазон длинных волн) передаются сигналы точного времени немецкой станции DCF77. По ним уже была отдельная статья, так что можно лишь кратко повторить, что это простой по структуре сигнал в амплитудной модуляции — разными длительностями закодированы «1» и «0», в итоге за одну минуту принимается 58-битный код.
image

130-140КГц — телеметрия электросетей


На этих частотах, если верить сайту radioscanner, передаются сигналы управления электросетями Германии.

Сигнал достаточно сильный, и по отзывам, принимается даже в Австралии. Декодировать его можно в MultiPSK, если выставить параметры, как показано на скриншоте.

На выходе мы получим пакеты данных, их структура разумеется, неизвестна, желающие могут поэкспериментировать и заняться анализом на досуге. Технически, сам сигнал очень прост, метод называется FSK (Frequency Shift Keying) и заключается в формировании битовой последовательности путем смены частоты передачи. Тот же сигнал, в виде спектра — биты можно посчитать даже вручную.

Метеотелетайп


На спектре выше, совсем рядом, на частоте 147КГц виден еще один сигнал. Это (также немецкая) станция DWD (Deutscher Wetterdienst), передающая сводки погоды для судов. Помимо этой частоты, сигналы передаются также на 11039 и 14467КГц.

Результат декодирования показан на скриншоте.

Принцип кодирования телетайпа такой же, FSK, интерес тут представляет кодирование текста. Оно 5-битное, с помощью кода Бодо, и имеет практически 100-летнюю историю.

Вроде бы аналогичный код использовался и на перфолентах, ну а телетайпы погоды рассылаются где-то с 60х годов, и как можно видеть, работают до сих пор. Разумеется, на реальном корабле сигнал не декодируется с помощью компьютера — существуют специальные приемники, которые записывают сигнал и выводят его на экран.

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

Метеофакс


Еще один legacy-сигнал примерно с почти такой же давней историей. В этом сигнале изображение передается в аналоговом виде со скоростью 120 линий в минуту (бывают и другие значения, например 60 или 240 LPM), для кодирования яркости используется частотная модуляция — яркость каждой точки изображения пропорциональна изменению частоты. Такая простая схема позволяла передавать изображения еще в те времена, когда про “цифровые сигналы” мало кто слышал.

Популярной в европейской части и удобной для приема является уже упомянутая немецкая станция DWD (Deutche Wetterdienst), передающая сообщения на частотах 3855, 7880 и 13882КГц. Другая организация, факсы которой несложно принять — британская Joint Operational Meteorology and Oceanography Centre, они передают сигналы на частотах 2618, 4610, 6834, 8040, 11086, 12390 и 18261КГц.

Чтобы принять сигналы HF Fax, нужно использовать режим приемника USB, для декодирования можно использовать MultiPSK. Результат приема через websdr-приемник показан на рисунке:

Эта картинка была принята прямо во время написания текста. Видно кстати, что вертикальные линии съехали — протокол аналоговый, и точность синхронизации здесь критична, даже небольшие задержки звука вызывают сдвиг изображения. При использовании “настоящего” приемника такого эффекта не будет.

Разумеется, как и в случае с метеотелетайпом, на судах никто не декодирует факсы с помощью компьютера — существуют специализированные приемники (пример картинки из начала статьи), которые делают всю работу автоматически.

STANAG 4285


Рассмотрим теперь более современный стандарт передачи данных на коротких волнах — модем Stanag 4285. Этот формат разрабатывался для NATO, и существует в различных вариантах. В основе лежит фазовая модуляция, параметры сигнала могут варьироваться, как можно видеть из таблицы, скорость может составлять от 75 до 2400бит/с. Это может показаться немного, но учитывая среду передачи — короткие волны, с их замираниями и помехами, это вполне хороший результат.

Программа MultiPSK может декодировать STANAG, но в 95% случаев результатом декодирования будет лишь “мусор” — сам формат предоставляет лишь побитовый протокол нижнего уровня, а собственно данные могут быть зашифрованы или иметь какой-то свой формат. Некоторые сигналы впрочем, декодировать можно, например, приведенная ниже запись на частоте 8453КГц. Декодировать хоть какой-то сигнал через websdr-приемник у меня не получилось, видимо все же онлайн-передача нарушает структуру данных. Желающие могут скачать файл с реального приемника по ссылке cloud.mail.ru/public/JRZs/gH581X71s. Результаты декодирования в MultiPSK показаны на скриншоте ниже. Как можно видеть, скорость для данной записи составляет 600bps, в качестве содержимого видимо, передается текстовый файл.

Интересно, что как можно видеть на панораме, таких сигналов в эфире реально очень много:

Разумеется, не все из них возможно, принадлежат именно STANAG — на похожих принципах существуют и другие протоколы. Для примера, можно привести разбор сигнала Thales HF Modem.

Как и в случае с другими рассмотренными сигналами, для реального приема и передачи используются специализированные устройства. К примеру для показанного на фото модема NSGDatacom 4539 заявлена скорость от 75 до 9600bps при полосе сигнала 3КГц.

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

Посмотрим кстати, внимательнее на панораму выше. Слева мы видим… правильно, старую добрую азбуку Морзе. Итак, переходим к следующему сигналу.

Код Морзе (CW)


На частоте 8423КГц мы слышим именно его. Искусство слухового приема азбуки Морзе сейчас практически утрачено, поэтому мы воспользуемся MultiPSK (впрочем, декодирует она так себе, программа CW Skimmer справляется гораздо лучше).

Как можно видеть, передается повторяющийся текст DE SVO, если верить сайту radioscanner, станция расположена в Греции.

Разумеется, таких сигналов уже мало, но они еще есть. Как пример, можно привести давно работающую станцию на 4331КГц, передающую повторяющиеся сигналы “VVV DE E4X4XZ”. Как подсказывает Гугл, станция принадлежит израильским ВМС. Передается ли на этой частоте что-то еще? Ответ неизвестен, желающие могут послушать и проверить самостоятельно.

The Buzzer (УВБ-76)


Завершает наш хит-парад самый наверное, известный, сигнал — известный и в России и за ее пределами, сигнал на частоте 4625КГц.

Сигнал используется для оповещения войск, и представляет собой повторяющиеся гудки, в перерывах между которыми иногда передаются кодовые фразы из шифроблокнота (абстрактные слова типа «КРОЛИСТ» или «БРАМИРКА»). Одни пишут что видели такие приемники в военкоматах, другие говорят что это часть системы «мертвая рука», в общем, сигнал является меккой для любителей Сталкера, теорий заговора, «холодной войны» и прочего-прочего. Желающие могут набрать в поиске «УВБ-76», и уверен, занимательное чтиво на вечер гарантировано (впрочем, не стоит ко всему написанному относиться серьезно). В то же время, система достаточно интересна, хотя бы тем что работает до сих пор со времен «холодной войны», хотя нужно ли это кому-то сейчас, сказать сложно.

Завершение


Данный список далеко не полон. С помощью радиоприемника можно услышать (а точнее увидеть) и сигналы связи с подводными лодками, и загоризонтные радары, и быстро меняющиеся frequency hopping сигналы, и много чего еще.

Вот для примера картинка, сделанная прямо сейчас на частоте 8МГц, на ней можно насчитать минимум 5 сигналов различных видов.

Что они из себя представляют — зачастую неизвестно, по крайней мере, в открытых источниках найти можно далеко не все (хотя есть такие сайты как www.sigidwiki.com/wiki/Signal_Identification_Guide и www.radioscanner.ru/base). Изучение таких сигналов вполне интересно как с точки зрения математики, программирования и ЦОС, так и просто как способ узнать что-то новое об окружающем мире.

Интересно и то, что несмотря на развитие Интернета и коммуникаций, радио не только не сдает позиции, а пожалуй даже наоборот — возможность передачи данных напрямую от отправителя получателю, без цензуры, контроля траффика и отслеживания пакетов, может стать (хотя будем надеяться что все же не станет) снова актуальной…

Let's block ads! (Why?)

Настраиваем удобную сборку проектов в Visual Studio

Эта статья является руководством по настройке сборки C++ проектов Visual Studio. Частично она сводилась из материалов разрозненных статей на эту тему, частично является результатом реверс-инжениринга стандартных конфигурационных файлов Студии. Я написал ее в основном потому что полезность документации от самой Microsoft на эту тему стремится к нулю и мне хотелось иметь под рукой удобный референс к которому в дальнейшем можно будет обращаться и отсылать других разработчиков. Visual Studio имеет удобные и широкие возможности для настройки по-настоящему удобной работы со сложными проектами и мне досадно видеть что из-за отвратительной документации эти возможности очень редко сейчас используются.

В качестве примера попробуем сделать так чтобы в Студию можно было добавлять flatbuffer schema, а Студия автоматически вызывала flatc в тех случаях когда это нужно (и не вызывала — когда изменений не было) и позволяла задавать настройки напрямую через File Properties

Оглавление


* Level 1: лезем внутрь .vcxproj файлов
     Поговорим о .props файлах
     Но зачем вообще разделять .vcxproj и .props?
     Делаем настройку проекта читабельнее
     Делаем удобным подключение сторонних библиотек
     Project Templates — автоматизируем создание проектов
* Level 2: настраиваем кастомную компиляцию
     Традиционный подход
     Знакомимся с MSBuild targets
     Попробуем создать target для сборки .proto файлов
     Доводим наш модельный пример до ума
     U2DCheck и tlog файлы
     Финализуем наш кастомный .target
     А что насчет CustomBuildStep?
     Правильное копирование файлов
* Level 3: интегрируемся с GUI от Visual Studio
     Вытаскиваем настройки из недр .vcxproj в Configuration Properties
     Объясняем Студии про новые типы файлов
     Ассоциируем настройки с индивидуальными файлами
* Level 4: расширяем функциональность MSBuild

ЗАМЕЧАНИЕ: все приведенные в статье примеры проверялись в VS 2017. В рамках моего понимания они должны работать и в более ранних версиях студии начиная по крайней мере с VS 2012, но обещать я этого не могу.

Level 1: лезем внутрь .vcxproj файлов


Давайте взглянем внутрь типичного .vcxproj автоматически сгенеренного Visual Studio.
Он будет выглядеть как-то примерно так
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Debug|x64">
      <Configuration>Debug</Configuration>
      <Platform>x64</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|x64">
      <Configuration>Release</Configuration>
      <Platform>x64</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <VCProjectVersion>15.0</VCProjectVersion>
    <ProjectGuid>{0D35456E-42DA-418B-87D4-55E32B8E1373}</ProjectGuid>
    <Keyword>Win32Proj</Keyword>
    <RootNamespace>protobuftest</RootNamespace>
    <WindowsTargetPlatformVersion>10.0.17134.0</WindowsTargetPlatformVersion>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <PlatformToolset>v141</PlatformToolset>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <PlatformToolset>v141</PlatformToolset>
    <WholeProgramOptimization>true</WholeProgramOptimization>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <PlatformToolset>v141</PlatformToolset>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <PlatformToolset>v141</PlatformToolset>
    <WholeProgramOptimization>true</WholeProgramOptimization>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
  <ImportGroup Label="ExtensionSettings">
  </ImportGroup>
  <ImportGroup Label="Shared">
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <PropertyGroup Label="UserMacros" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <LinkIncremental>true</LinkIncremental>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <LinkIncremental>true</LinkIncremental>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <LinkIncremental>false</LinkIncremental>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <LinkIncremental>false</LinkIncremental>
  </PropertyGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>Disabled</Optimization>
      <SDLCheck>true</SDLCheck>
      <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ConformanceMode>true</ConformanceMode>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
    </ClCompile>
    <Link>
      <SubSystem>Console</SubSystem>
      <GenerateDebugInformation>true</GenerateDebugInformation>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>Disabled</Optimization>
      <SDLCheck>true</SDLCheck>
      <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ConformanceMode>true</ConformanceMode>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
    </ClCompile>
    <Link>
      <SubSystem>Console</SubSystem>
      <GenerateDebugInformation>true</GenerateDebugInformation>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>MaxSpeed</Optimization>
      <FunctionLevelLinking>true</FunctionLevelLinking>
      <IntrinsicFunctions>true</IntrinsicFunctions>
      <SDLCheck>true</SDLCheck>
      <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ConformanceMode>true</ConformanceMode>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
    </ClCompile>
    <Link>
      <SubSystem>Console</SubSystem>
      <EnableCOMDATFolding>true</EnableCOMDATFolding>
      <OptimizeReferences>true</OptimizeReferences>
      <GenerateDebugInformation>true</GenerateDebugInformation>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>MaxSpeed</Optimization>
      <FunctionLevelLinking>true</FunctionLevelLinking>
      <IntrinsicFunctions>true</IntrinsicFunctions>
      <SDLCheck>true</SDLCheck>
      <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ConformanceMode>true</ConformanceMode>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
    </ClCompile>
    <Link>
      <SubSystem>Console</SubSystem>
      <EnableCOMDATFolding>true</EnableCOMDATFolding>
      <OptimizeReferences>true</OptimizeReferences>
      <GenerateDebugInformation>true</GenerateDebugInformation>
    </Link>
  </ItemDefinitionGroup>
  <ItemGroup>
    <ClInclude Include="pch.h" />
  </ItemGroup>
  <ItemGroup>
    <ClCompile Include="pch.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="protobuf_test.cpp" />
  </ItemGroup>
  <ItemGroup>
    <Text Include="test.proto" />
  </ItemGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
  <ImportGroup Label="ExtensionTargets">
  </ImportGroup>
</Project>

Довольно нечитаемое месиво, не правда ли? И это ведь еще очень небольшой и практически тривиальный файл. Попробуем превратить его во что-то более читабельное и удобное для восприятия.

Поговорим о .props файлах


Для этого обратим пока внимание на то что взятый нами файл — это обычный XML-документ и его можно логически разделить на две части, в первой из которых перечисляются настройки проекта, а во второй — входящие в него файлы. Давайте эти половинки разделим физически. Для этого нам понадобится уже встречающийся в коде тэг Import который является аналогом сишного #include и позволяет включить один файл в другой. Скопируем наш .vcxproj в какой-нибудь другой файл и уберем из него все объявления относящиеся к файлам входящим в проект, а из .vcxproj-а в свою очередь наоборот уберем все кроме объявлений относящихся к файлам собственно входящим в проект. Получившийся у нас файл с настройками проекта но без файлов в Visual Studio принято называть Property Sheets и сохранять с расширением .props. В свою очередь в .vcxproj мы поставим соответствующий Import
Теперь .vcxproj описывает только файлы входящие в проект и читается намного легче
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="settings.props" />
  <PropertyGroup Label="Globals">
    <ProjectGuid>{0D35456E-42DA-418B-87D4-55E32B8E1373}</ProjectGuid>
  </PropertyGroup>
  <ItemGroup>
    <ClInclude Include="pch.h" />
  </ItemGroup>
  <ItemGroup>
    <ClCompile Include="pch.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="protobuf_test.cpp" />
  </ItemGroup>
  <ItemGroup>
    <Text Include="test.proto" />
  </ItemGroup>
</Project>

Его можно упростить еще больше, убрав лишние XML-элементы. К примеру свойство «PrecompiledHeader» объявляется сейчас 4 раза для разных вариантов конфигурации (release / debug) и платформы (win32 / x64) но каждый раз это объявление одно и то же. Кроме того у нас здесь используется несколько разных ItemGroup тогда как в реальности вполне достаточно одного элемента. В результате приходим к компактному и понятному .vcxproj который просто перечисляет 1) входящие в проект файлы, 2) то чем является каждый из них (плюс настройки специфичные для конкретных отдельных файлов) и 3) содержит в себе ссылку на хранящиеся отдельно настройки проекта.
<?xml version="1.0" encoding="utf-8"?><Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="settings.props" />
  <PropertyGroup Label="Globals">
    <ProjectGuid>{0D35456E-42DA-418B-87D4-55E32B8E1373}</ProjectGuid>
  </PropertyGroup>
  <ItemGroup>
    <ClInclude Include="pch.h" />
    <ClCompile Include="pch.cpp">
      <PrecompiledHeader>Create</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="protobuf_test.cpp" />
    <Text Include="test.proto" />
  </ItemGroup>
</Project>

Перезагружаем проект в студии, проверяем сборку — все работает.

Но зачем вообще разделять .vcxproj и .props?


Поскольку в сборке ничего не поменялось, то на первый взгляд может показаться что мы поменяли шило на мыло, сделав бессмысленный «рефакторинг» файла в который нам до этого собственно и не было никакой нужды заглядывать. Однако допустим на минутку что в наш solution входит более одного проекта. Тогда, как несложно заметить, несколько разных .vcxproj-файлов от разных проектов могут использовать один и тот же .props файл с настройками. Мы отделили правила сборки используемые в solution от исходного кода и можем теперь менять настройки сборки для всех однотипных проектов в одном месте. В подавляющем большинстве случаев подобная унификация сборки — это хорошая идея. К примеру добавляя в solution новый проект мы в одно действие тривиально перенесем в него подобным образом все настройки из уже существующих в solution проектов.

Но что если нам все же нужны разные настройки для разных проектов? В этом случае мы можем просто создать несколько разных .props-файлов для разных типов проектов. Поскольку .props-файлы могут совершенно аналогичным образом Import-ить другие .props-файлы, то довольно легко и естественно можно выстроить «иерархию» из нескольких .props-файлов, от файлов описывающих общие настройки для всех проектов в solution до узкоспециализированных версий задающих специальные правила для всего одного-двух проектов в solution. В MSBuild действует правило что если одна и та же настройка объявляется во входном файле дважды (скажем вначале импортится в base.props а затем объявляется повторно в derived.props который import-ит в своем начале base.props) то более позднее объявление перекрывает более раннее. Это позволяет легко и удобно задавать произвольные иерархии настроек просто перекрывая в каждом .props файле все необходимые для данного .props-а настройки не заботясь о том что они могли быть где-то уже объявлены ранее. В числе прочего где-нибудь в .props-ах разумно импортировать стандартные настройки окружения Студии которые для C++-проекта будут выгледеть вот так:

<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />

Отмечу что на практике весьма удобно класть собственные .props файлы в ту же папку что и .sln файл
Поскольку это позволяет удобно импортировать .props независимо от местоположения .vcxproj
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ...>
  <Import Project="$(SolutionDir)\settings.props" />
   ...
</Project>

Делаем настройку проекта читабельнее


Теперь когда нам больше не требуется возиться с каждым проектом по отдельности мы можем уделить больше внимания настройке процесса сборки. И для начала я рекомендую дать с помощью .props-файлов вменяемые имена большинству интересных объектов в файловой системе относящихся к solution. Для этого нам следует создать тэг PropertyGroup с пометкой UserMacros:
<PropertyGroup Label="UserMacros">
    <RepositoryRoot>$(SolutionDir)\..</RepositoryRoot>
    <ProjectsDir>$(RepositoryRoot)\projects</ProjectsDir>
    <ThirdPartyDir>$(RepositoryRoot)\..\ThirdParty</ThirdPartyDir>
    <ProtoBufRoot>$(ThirdPartyDir)\protobuf\src</ProtoBufRoot>
  </PropertyGroup>


Тогда в настройках проектов вместо конструкций вида "..\..\..\ThirdParty\protobuf\src\protoc.exe" мы сможем написать просто "$(ProtoBufRoot)\protoc.exe". Помимо большей читабельности это делает код намного мобильнее — мы можем свободно перемещать .vcxproj не боясь что у него слетят настройки и можем перемещать (или обновлять) Protobuf изменив всего одну строчку в одном из .props файлов.

При последовательном объявлении нескольких PropertyGroups их содержимое будет объединено — перезапишутся только макросы имена которых совпадают с ранее объявлявшимися. Это позволяет легко дополнять объявления во вложенных .props файлах не боясь потерять макросы уже объявленные ранее.

Делаем удобным подключение сторонних библиотек


Обычный процесс включения зависимости от thirdparty-библиотеки в Visual Studio частенько выглядит примерно вот так:

Процесс соответствующей настройки включает в себя редактирование сразу нескольких параметров находящихся на разных вкладках настроек проекта и потому довольно зануден. Вдобавок его обычно приходится проделывать по нескольку раз для каждой отдельно взятой конфигурации в проекте, так что нередко в результате подобных манипуляций оказывается что проект в Release-сборке собирается, а в Debug-сборке — нет. Так что это неудобный и ненадежный подход. Но как Вы наверное уже догадываетесь, те же самые настройки можно «упаковать» в props-файл. К примеру для библиотеки ZeroMQ подобный файл может выглядеть примерно так:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemDefinitionGroup>
    <ClCompile>
      <AdditionalIncludeDirectories>$(ThirdPartyDir)\libzmq\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
      <PreprocessorDefinitions>ZMQ_STATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    </ClCompile>
    <Link>
      <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">libzmq-v120-mt-sgd-4_3_1.lib;Ws2_32.Lib;%(AdditionalDependencies)</AdditionalDependencies>
      <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">libzmq-v120-mt-s-4_3_1.lib;Ws2_32.Lib;%(AdditionalDependencies)</AdditionalDependencies>
      <AdditionalLibraryDirectories>$(ThirdPartyDir)\libzmq\lib\x64\$(Configuration);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
    </Link>
  </ItemDefinitionGroup>
</Project>

Обратите внимание что если мы просто определим тэг типа AdditionalLibraryDirectories в props-файле, то он перекроет все более ранние определения. Поэтому здесь используется чуть более сложная конструкция в которой тэг завершается последовательностью символов ;%(AdditionalLibraryDirectories) образующих ссылку тэга самого на себя. В семантике MSBuild этот макрос раскрывается в предыдущее значение тэга, так что подобная конструкция дописывает параметры в начало строки хранящейся в парамере AdditionalLibraryDirectories.

Для подключения ZeroMQ теперь достаточно просто импортировать данный .props файл.

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ...>
  <Import Project="$(SolutionDir)\settings.props" />
  <Import Project="$(SolutionDir)\zeromq.props" />
   ...
</Project>

И на этом манипуляции с проектом заканчиваются — MSBuild автоматически подключит необходимые заголовочные файлы и библиотеки и в Release и в Debug сборках. Таким образом потратив немного времени на написание zeromq.props мы получаем возможность надежно и безошибочно подключать ZeroMQ к любому проекту всего в одну строчку. Создатели Студии даже предусмотрели для этого специальный GUI который называется Property Manager, так что любители мышки могут проделать ту же операцию в несколько кликов.

Правда как и остальные инструменты Студии этот GUI вместо читабельного однострочника добавит в код .vcxproj что-то вроде

вот такого кода
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <Import Project="..\..\..\..\projects\BuildSystem\thirdparty_libraries\PropertySheets\zeromq.props" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <Import Project="..\..\..\..\projects\BuildSystem\thirdparty_libraries\PropertySheets\zeromq.props" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <Import Project="..\..\..\..\projects\BuildSystem\thirdparty_libraries\PropertySheets\zeromq.props" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <Import Project="..\..\..\..\projects\BuildSystem\thirdparty_libraries\PropertySheets\zeromq.props" />
  </ImportGroup>


Так что я предпочитаю добавлять ссылки на сторонние библиотеки в .vcxproj файлы вручную.

Аналогично тому что уже обсуждалось ранее, работа с ThirdParty-компонентами через .props файлы позволяет так же легко в дальнейшем обновлять используемые библиотеки. Достаточно отредактировать единственный файл zeromq.props — и сборка всего solution синхронно переключится на новую версию. К примеру в наших проектах сборка проекта через этот механизм увязана с менеджером зависимостей Conan который собирает необходимый набор thirdparty-библиотек по манифесту зависимостей и автоматически генерирует соответствующие .props-файлы.

Project Templates — автоматизируем создание проектов


Править вручную .vcxproj-файлы созданные Студией конечно довольно скучно (хотя при наличии навыка и недолго). Поэтому в Студии предусмотрена удобная возможность по созданию собственных шаблонов для новых проектов, которые позволяют провести ручную работу по настройке .vcxproj лишь один раз, после чего повторно использовать ее одним кликом в любом новом проекте. В простейшем случае для этого даже не надо ничего править вручную — достаточно открыть проект который нужно превратить в шаблон и выбрать в меню Project \ Export Template. В открывшемся диалоговом окне можно задать несколько тривиальных параметров вроде имени для шаблона или строки которая будет показываться в его описании, а так же выбрать, будет ли вновь созданный шаблон сразу добавлен в диалоговое окно «New Project». Созданный таким способом шаблон создает копию использованного для его создания проекта (включая все файлы входящие в проект), заменяя в нем только имя проекта и его GUID. В довольно большом проценте случаев этого более чем достаточно.

При более детальном рассмотрении сгенерированного Студией шаблона можно легко убедиться в том что это просто zip архив в котором лежат все файлы использованные в шаблоне и один дополнительный конфигурационный файл с расширением .vstemplate. В этом файле хранится список метаданных проекта (вроде использованной иконки или строки с описанием) и список файлов которые необходимо создать при создании нового проекта. Например,

<VSTemplate Version="3.0.0" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005" Type="Project">
  <TemplateData>
    <Name>C++ console application</Name>
    <Description>C++ console application for our project</Description>
    <ProjectType>VC</ProjectType>
    <ProjectSubType>
    </ProjectSubType>
    <SortOrder>1000</SortOrder>
    <CreateNewFolder>true</CreateNewFolder>                  `
    <DefaultName>OurCppConsoleApp</DefaultName>
    <ProvideDefaultName>true</ProvideDefaultName>
    <LocationField>Enabled</LocationField>
    <EnableLocationBrowseButton>true</EnableLocationBrowseButton>
    <Icon>ng.ico</Icon>
  </TemplateData>
  <TemplateContent>
    <Project TargetFileName="$projectname$.vcxproj" File="console_app.vcxproj" ReplaceParameters="true">
      <ProjectItem ReplaceParameters="false" TargetFileName="$projectname$.vcxproj.filters">console_app.vcxproj.filters</ProjectItem>
      <ProjectItem ReplaceParameters="false" TargetFileName="main.cpp">main.cpp</ProjectItem>
      <ProjectItem ReplaceParameters="false" TargetFileName="stdafx.cpp">stdafx.cpp</ProjectItem>
      <ProjectItem ReplaceParameters="false" TargetFileName="stdafx.h">stdafx.h</ProjectItem>
    </Project>
  </TemplateContent>
</VSTemplate>

Обратите внимание на параметр ReplaceParameters=«true». В данном случае он применяется только к vcxproj-файлу который выглядит следующим образом:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(SolutionDir)\console_app.props" />
  <PropertyGroup Label="Globals">
    <ProjectGuid>{$guid1$}</ProjectGuid>
    <RootNamespace>$safeprojectname$</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <ClCompile Include="main.cpp" />
    <ClCompile Include="stdafx.cpp">
      <PrecompiledHeader>Create</PrecompiledHeader>
    </ClCompile>
  </ItemGroup>
  <ItemGroup>
    <ClInclude Include="stdafx.h" />
  </ItemGroup>
</Project>

На месте GUID и RootNamespace как видите стоят не конкретные значения, а «заглушки» $guid1$ и $safeprojectname$. При использовании шаблона, Студия проходит по файлам помеченным ReplaceParamters=«true», ищет в них заглушки вида $name$ и заменяет их на вычисляемые значения по специальному словарю. По умолчанию Студия поддерживает не очень много параметров, но при написании Visual Studio Extensions (о чем мы поговорим чуть позже) туда нетрудно добавить сколько угодно своих собственные параметров вычисляемых (или вводимых пользователем) при запуске диалога по созданию нового проекта из шаблона. Как можно увидеть в файле .vstemplate, тот же словарь может использоваться и для формирования имени файла что позволяет, в частности, сформировать шаблону уникальные имена .vcxproj-файлов для разных проектов. При задании ReplaceParameters=false файл указанный в шаблоне будет просто скопирован без дополнительной обработки.

Полученный ZIP-архив с шаблоном можно добавить в список шаблонов известных Студии одним из нескольких способов. Проще всего просто скопировать этот файл в папку %USERPROFILE%\Documents\Visual Studio XX\Templates\ProjectTemplates. Стоит заметить, что несмотря на то что в этой папке Вы найдете множество разных подпапок совпадающих по названиям с папками в окне создания нового проекта, по факту шаблон следует положить просто в корневую папку поскольку положение шаблона в дереве новых проектов определяется Студией из тэгов ProjectType и ProjectSubType в .vstemplate-файле. Этот способ удобнее всего подходит для создания «персональных» шаблонов уникальных только для Вас и если Вы выберете в диалоге Export Template галочку «Automatically import template into Visual Studio» то Студия именно это и сделает, поместив созданный при экспорте zip-архив в эту папку с шаблонами. Однако делиться такими шаблонами с коллегами путем их ручного копирования конечно не очень удобно. Поэтому давайте познакомимся с чуть более продвинутым вариантом — создадим Visual Studio Extension (.vsix)

Для создания VSIX нам понадобится установить опциональный компонент Студии который так и называется — средства для разработки Visual Studio Extensions:

После этого в разделе Visual C# \ Extensibility появится вариант «VSIX project». Обратите внимание, что несмотря на свое расположение (C#), он используется для создания любых расширений, в том числе и наборов шаблонов проектов на C++.

В созданном VSIX проекте можно делать массу самых разных вещей — к примеру, создать свое собственное диалоговое окно которое будет использоваться для настройки создаваемых по шаблону проектов. Но это отдельная огромная тема для обсуждения которую я не буду в этой статье затрагивать. Для создания же шаблонов в VSIX все устроено предельно просто: создаем пустой VSIX проект, открываем файл .vsixmanifest и прямо в GUI задаем все данные для проекта. Вписываем метаданные (название расширения, описание, лицензия) на вкладке Metadata. Обратите внимание на расположенное в правом верхнем углу поле «Version» — его желательно указать правильно, поскольку Студия впоследствии использует именно его для определения того какая версия расширения установлена на компьютере. Затем идем на вкладку Assets и выбираем «Add new Asset», с Type: Microsoft.VisualStudio.ProjectTemplate, Source: File on filesystem, Path: (имя к zip-архиву с шаблоном). Нажимаем OK, повторяем процесс пока не добавим в VSIX все желаемые шаблоны.

После этого остается выбрать Configuration: Release и скомандовать Build Solution. Код писать не требуется, править конфигурационные файлы вручную — тоже. На выходе получается переносимый файл с расширением .vsix, который является, по сути, инсталлятором для созданного нами расширения. Созданный файл будет «запускаться» на любом компьютере с установленной Студией, показывать диалог с описанием расширения и лицензией и предлагать установить его содержимое. Разрешив установку — получаем добавление наших шаблонов в диалоговое окно «Создать новый проект»

Подобный подход позволяет легко унифицировать работу большого количества человек над проектом. Для установки и использования шаблонов от пользователя не требуется никакой квалификации кроме пары кликов мышкой. Установленное расширение можно посмотреть (и удалить) в диалоге Tools \ Extensions and Updates

Level 2: настраиваем кастомную компиляцию


ОК, на этом этапе мы разобрались как организованы vcxproj и props файлы и научились их организовывать. Давайте теперь предположим что мы хотим добавить в наш проект парочку .proto схем для сериализации объектов на основе замечательной библиотеки Google Protocol Buffers. Напомню основную идею этой библиотеки: Вы пишите описание объекта («схему») на специальном платформонезависимом мета-языке (.proto-файл) которая компилируется специальным компилятором (protoc.exe) в набор .cpp / .cs / .py / .java / etc. файлов которые реализуют сериализацию / десериализацию объектов по этой схеме в нужном языке программирования и которые Вы можете использовать в своём проекте. Таким образом при компиляции проекта нам нужно первым делом позвать protoc который создаст для нас набор .cpp файлов которые мы в дальнейшем будем использовать.

Традиционный подход


Классическая реализация «в лоб» прямолинейна и состоит в том чтобы просто добавить вызов protoc в pre-build step для проекта которому нужны .proto-файлы. Примерно вот так:

Но это не очень удобно:

  • Требуется явно указывать список обрабатываемых файлов в команде
  • При изменении этих файлов билд НЕ будет пересобран автоматически
  • При изменении ДРУГИХ файлов в проекте которые Студия распознает как исходные коды, напротив, без нужды будет выполнен pre-build step
  • Сгенерированные файлы не входят по умолчанию в сборку проекта
  • Если мы включим сгенерированные файлы в проект вручную, то проект будет выдавать ошибку когда мы его будем открывать в первый раз (поскольку файлы еще не сгенерированы первой сборкой).

Вместо этого мы попробуем «объяснить» самой Visual Studio (а точнее используемой ею системе сборки MSBuild) то как следует обрабатывать подобные .proto-файлы.

Знакомимся с MSBuild targets


С точки зрения MSBuild, сборка любого проекта состоит из последовательности сборки сущностей которые называются build targets, сокращенно targets. К примеру сборка проекта может включать в себя выполнение таргета Clean который удалит оставшиеся от предыдущих билдов временные файлы, затем выполнение таргета Compile который скомпилирует проект, затем таргета Link и наконец таргета Deploy. Все эти таргеты вместе с правилами по их сборке не фиксированы заранее а определяются в самом .vcxproj файле. Если Вы знакомы с nix-овой утилитой make и Вам на ум в этот момент приходит слово «makefile», то Вы совершенно правы: .vcxproj является XML-вариацией на тему makefile.

Но стоп-стоп-стоп скажет тут сбитый с толку читатель. Как это так? Мы просмотрели до этого .vcxproj в простом проекте и там не было ни target-ов ни какого-либо сходства с классическим makefile. О каких target-ах тогда может идти речь? Оказывается что они просто «спрятаны» вот в этой строчке включающей в .vcxproj набор стандартных target-ов для сборки C++ — кода.

<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />

«Стандартный» билд-план предлагаемый Студией довольно обширен и предлагает большой набор правил для компиляции C++-кода и «стандартных» таргетов типа Build, Clean и Rebuild к которым умеет «цепляться» Студия. Этот набор часто известен под собирательным названием toolset и заменяя в импорте toolset можно заставить Студию компилировать один и тот же проект с помощью другой версии Студии или, к примеру, интеловским компилятором или Clang. Кроме того при желании от стандартного toolset-а можно вообще отказаться и написать свой собственный toolset с нуля. Но мы будем рассматривать в этой статье более простой вариант в котором мы ничего не будем заменять, а лишь дополним стандартные правила необходимыми нам дополнениями.

Но вернемся обратно к target-ам. Любой target в MSBuild определяется через

  • Список входов (inputs)
  • Список выходов (outputs)
  • Зависимости от других targets (dependencies)
  • Настройки target-а
  • Последовательность фактических шагов выполняемых target-ом (tasks)

Например таргет ClCompile получает на вход список .cpp файлов в проекте и генерирует из них путем таски вызывающей компилятор cl.exe набор .obj файлов. Настройки таргета ClCompile при этом превращаются в флаги компиляции передаваемые cl.exe. Когда мы пишем в .vcxproj файле строчку
<ClCompile Include="protobuf_test.cpp" />

то мы добавляем (Include) файл protobuf_tests.cpp в список входов (inputs) данного таргета, а когда пишем
<ItemDefinitionGroup>
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
    </ClCompile>
</ItemDefinitionGroup>

то присваем значение «Use» настройке ClCompile.PrecompiledHeader которую target затем превратит в флаг /Yu переданный компилятору cl.exe.

Попробуем создать target для сборки .proto файлов


Добавление нового target-а реализуется с помощью тэга target:
<Target Name="GenerateProtobuf">
...steps to take...
</Target>

Традиционно target-ы выносят в подключаемый файл с расширением .targets. Не то чтобы это было строго необходимо (и vcxproj и targets и props файлы внутри являются равнозначным XML-ем), но это стандартная схема именования и мы будем ее придерживаться. Чтобы в коде .vcxproj файла теперь можно было писать что-то вроде
<ItemGroup>
    <ClInclude Include="cpp.h"/>
    <ProtobufFile Include="my.proto" />
<ItemGroup>

созданный нами target необходимо добавить в список AvailableItemName
<ItemGroup>
    <AvailableItemName Include="ProtobufFile">
      <Targets>GenerateProtobuf</Targets>
    </AvailableItemName>
</ItemGroup>

Нам также понадобится описать что же конкретно мы хотим сделать с нашими входными файлами и что должно получиться на выходе. Для этого в MSBuild используется сущность которая называется «task». Таска — это какое-то простое действие которое нужно сделать в ходе сборки проекта. К примеру «создать директорию», «скомпилировать файл», «запустить команду», «скопировать что-то». В нашем случае мы воспользуемся таской Exec чтобы запустить protoc.exe и таской Message чтобы отобразить этот шаг в логе компиляции. Укажем так же что запуск данного target-а следует провести сразу после стандартного таргета PrepareForBuild. В результате у нас получится примерно вот такой файлик protobuf.targets
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>  
    <AvailableItemName Include="ProtobufSchema">
      <Targets>GenerateProtobuf</Targets>
    </AvailableItemName>
  </ItemGroup>
  <Target Name="GenerateProtobuf"
     Inputs="%(ProtobufSchema.FullPath)"
     Outputs=".\generated\%(ProtobufSchema.Filename).pb.cc"
     AfterTargets="PrepareForBuild">
     <Message Importance="High" Text="Compiling schema %(ProtobufSchema.Identity)" />
     <Exec Command="$(Protoc) --cpp_out=.\generated %(ProtobufSchema.Identity)" />
  </Target>
</Project>

Мы использовали здесь довольно нетривиальный оператор "%" (batching operator) который означает «для каждого элемента из списка» и автоматически добавляемые метаданные. Идея тут в следующем: когда мы записываем код вида
<ItemGroup>
  <ProtobufSchema Include="test.proto">
    <AdditionalData>Test</AdditionalData>
  </ProtobufSchema>
</ItemGroup>

то мы этой записью добавляем в список с названием «ProtobufSchema» дочерний элемент «test.proto» у которого есть дочерний элемент (метадата) AdditionalData содержащая строку «Test». Если мы напишем «ProtobufSchema.AdditionalData» то мы получим доступ к записи «Test». При этом помимо явно объявленных нами метаданных AdditionalData, хитрый MSBuild ради нашего удобства автоматически добавляет к записи еще добрый десяток полезных часто используемых дочерних элементов описанных вот здесь из числа которых мы использовали Identity (исходная строка), Filename (имя файла без расширения) и FullPath (полный путь к файлу). Запись же со знаком % заставляет MSBuild применить описанную нами операцию к каждому элементу из списка — т.е. к каждому .proto файлу по отдельности.

Добавляем теперь

  <Import Project="protobuf.targets" Label="ExtensionTargets"/>

в protobuf.props, переписываем наши proto-файлы в .vcxproj-е на тэг ProtobufSchema
  <ItemGroup>
    ...
    <ProtobufSchema Include="test.proto" />
    <ProtobufSchema Include="test2.proto" />
  </ItemGroup>


и проверяем сборку

1>------ Rebuild All started: Project: protobuf_test, Configuration: Debug x64 ------
1>Compiling schema test.proto
1>Compiling schema test2.proto
1>pch.cpp
1>protobuf_test.cpp
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

Ура! Заработало! Правда наши .proto файлы теперь стали не видны в проекте. Лезем в .vcxproj.filters и вписываем там по аналогии

...
<ItemGroup>
    <ProtobufSchema Include="test.proto">
      <Filter>Resource Files</Filter>
    </ProtobufSchema>
    <ProtobufSchema Include="test2.proto">
      <Filter>Resource Files</Filter>
    </ProtobufSchema>
  </ItemGroup>
...

Перезагружаем проект — файлы снова видны.

Доводим наш модельный пример до ума


Впрочем на самом деле я немного схитрил. Если не создать вручную перед началом билда папку generated, то билд на самом деле падает

1>...\protobuf_test\protobuf.targets(13,6): error MSB3073: The command "...\ThirdParty\protobuf\bin\protoc.exe --cpp_out=.\generated test.proto" exited with code 1.

Чтобы это исправить добавим вспомогательный target который создаст необходимую папку

...
<Target Name="PrepareToGenerateProtobuf"
     Inputs="@(ProtobufSchema)"
     Outputs=".\generated">
    <MakeDir Directories=".\generated"/>
</Target>
<Target Name="GenerateProtobuf"
     DependsOnTargets="PrepareToGenerateProtobuf"
...

С помощью свойства DependsOnTargets мы указываем что перед тем как запускать любую из задач GenerateProtobuf следует запустить PrepareToGenerateProtobuf, а запись @(ProtobufSchema) ссылается на список ProtobufSchema целиком, как единую сущность используемую как вход для этой задачи, так что запущена она будет лишь один раз.

Перезапускам сборку — работает! Давайте попробуем сделать теперь еще раз Rebuild, чтобы уж на этот раз точно во всем убедиться

1>------ Rebuild All started: Project: protobuf_test, Configuration: Debug x64 ------
1>pch.cpp
1>protobuf_test.cpp
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

Эм, а куда же пропали наши новые таски? Небольшая отладка — и мы видим что таски на самом деле запускаются MSBuild, но не выполняются поскольку в указанной нами выходной папке уже есть сгенерированные файлы. Проще говоря в Rebuild у нас не работает Clean для .\generated файлов. Исправим это, добавив еще один таргет

<Target Name="CleanProtobuf"
   AfterTargets="Clean">
    <RemoveDir Directories=".\generated"/>
  </Target>

Проверяем — работает. Clean очищает созданные нами файлы, Rebuild пересоздает их заново, повторный вызов Build не запускает без нужды пересборку еще раз.

========== Build: 0 succeeded, 0 failed, 1 up-to-date, 0 skipped ==========

Вносим правку в один из C++ файлов, пробуем сделать Build еще раз

1>------ Build started: Project: protobuf_test, Configuration: Debug x64 ------
1>protobuf_test.cpp
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

.proto-файл не менялся, поэтому protoc не перезапускался, все ожидаемо. Пробуем теперь изменить .proto файл.

========== Build: 0 succeeded, 0 failed, 1 up-to-date, 0 skipped ==========

Интересно что если запустить сборку MSBuild через командную строку вручную, а не через UI из Студии то такой проблемы не будет — MSBuild корректно пересоберет необходимые .pp.cc файлы. Если мы поменяем какой-нибудь .cpp то запустившийся в студии MSBuild пересоберет не только его, но и .props файл который мы меняли раньше

1>------ Build started: Project: protobuf_test, Configuration: Debug x64 ------
1>Compiling schema test.proto
1>protobuf_test.cpp
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

В чем же дело?

U2DCheck и tlog файлы


Оказывается что создатели Visual Studio посчитали что вызывать MSBuild на каждый чих слишком накладно и… реализовали собственную «быструю проверку» на то нужно ли собирать проект или нет. Она называется U2DCheck и если по ее мнению проект не менялся, то Студия просто не станет запускать MSBuild для этого проекта. Обычно U2DCheck работает настолько «тихо» что про ее существование мало кто догадывается но в реестре можно включить полезный флажок который заставит U2DCheck выводить более подробные отчеты.

В своей работе U2DCheck опирается на специальные .tlog файлы. Их легко можно найти в intermediate-output папке (имя_проекта).tlog и чтобы U2DCheck корректно реагировал на изменения в исходных файлах нам надо сделать в этой папке запись в один из read tlog — файлов, а чтобы U2DCheck корректно реагировал на удаление выходных файлов — запись в одном из write tlog — файлов.

Чертыхнувшись, возвращаемся к соответствующей правке нашего target-а

...
<Exec Command="$(Protoc) --cpp_out=.\generated %(ProtobufSchema.Identity)" />
<WriteLinesToFile
       File="$(TLogLocation)\protobuf.read.1.tlog"
       Lines="^%(ProtobufSchema.FullPath)" />

Проверяем — работает: правка .props файла триггерит необходимый ребилд, сборка в отсутствие правки показывает что проект up-to-date. В данном примере для простоты я не стал писать write tlog отслеживающий удаление созданных при компиляции файлов, но он добавляется в target аналогичным образом.

Начиная с Visual Studio 2017 update 15.8 в MSBuild была добавлена новая стандартная таска GetOutOfDateItems которая автоматизирует эту черную магию, но поскольку это произошло совсем недавно то практически все кастомные .target-ы продолжают работать с .tlog файлами вручную.

При желании можно так же полностью отключить U2DCheck для любого проекта добавив одну строчку в поле ProjectCapability

<ItemGroup>
  <ProjectCapability Include="NoVCDefaultBuildUpToDateCheckProvider" />
</ItemGroup>

Однако в этом случае Студия будет гонять MSBuild для этого проекта и всех зависящих от него при каждой сборке и да, U2DCheck добавляли не просто так — это работает не так быстро как мне хотелось бы.

Финализуем наш кастомный .target


Получившийся у нас результат вполне работоспособен, но его есть еще куда совершенствовать. К примеру в MSBuild существует режим «выборочной сборки» когда в командной строке указывается что требуется собрать не весь проект в целом, а лишь отдельные конкретно выбранные в нем файлы. Поддержка этого режима требует чтобы таргет проверял содержимое списка @(SelectedFiles).

Помимо этого в нашем исходном коде встречается довольно много повторяющих друг друга строк которые должны совпадать. Правила хорошего тона рекомендуют дать всем этим сущностям читабельные имена и адресовать по этим именам. Для этого часто создается отдельный специальный таргет который создает и заполняет вспомогательный список всеми именами которые понадобятся в дальнейшем.

Наконец мы все еще не реализовали обещанную в самом начале задумку — автоматическое включение сгенерированных файлов в проект. Мы уже можем #include-ить сгенерированные protobuf-ом заголовочные файлы зная что они будут автоматически созданы до того как дело дойдет до компиляции, но с линковщиком этот номер не проходит :). Поэтому просто дописываем сгенерированные файлы в список ClCompile.

Пример подобной причесанной реализации protobuf.targets
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>  
    <AvailableItemName Include="ProtobufSchema">
      <Targets>GenerateProtobuf</Targets>
    </AvailableItemName>
  </ItemGroup>
  <PropertyGroup>
    <ProtobufOutputFolder>.\generated</ProtobufOutputFolder>
  </PropertyGroup>

  <Target Name="ComputeProtobufInput">
    <ItemGroup>  
      <ProtobufCompilerData Include="@(ProtobufSchema)">
        <OutputCppFile>$(ProtobufOutputFolder)\%(ProtobufSchema.Filename).pb.cc</OutputCppFile>
        <OutputPythonFile>$(ProtobufOutputFolder)\%(ProtobufSchema.Filename)_pb2.py</OutputPythonFile>
        <OutputFiles>%(ProtobufCompilerData.OutputCppFile);%(ProtobufCompilerData.OutputPythonFile)</OutputFiles>
      </ProtobufCompilerData>
      <ClCompile Include="%(ProtobufCompilerData.OutputCppFile)">
        <PrecompiledHeader>NotUsing</PrecompiledHeader>
      </ClCompile>
    </ItemGroup>
  </Target>

  <Target Name="PrepareToGenerateProtobuf" Condition="'@(ProtobufSchema)'!=''"
     Inputs="@(ProtobufSchema)"
     Outputs="$(ProtobufOutputFolder)">
    <MakeDir Directories="$(ProtobufOutputFolder)"/>
  </Target>
  
  <Target Name="GenerateProtobuf"
     DependsOnTargets="PrepareToGenerateProtobuf;ComputeProtobufInput"
     Inputs="%(ProtobufCompilerData.FullPath)"
     Outputs="%(ProtobufCompilerData.OutputFiles)"
     AfterTargets="PrepareForBuild"
     BeforeTargets="Compile">
     <Message Importance="High" Text="Compiling schema %(ProtobufCompilerData.Identity)" />
     <Exec Command="$(Protoc) --cpp_out=$(ProtobufOutputFolder) --python_out=$(ProtobufOutputFolder) %(ProtobufCompilerData.Identity)">
       <Output ItemName="GeneratedFiles" TaskParameter="Outputs"/>
     </Exec>
     <WriteLinesToFile
       File="$(TLogLocation)\protobuf.read.1.tlog"
       Lines="^%(ProtobufCompilerData.FullPath)" />
  </Target>
  <Target Name="CleanProtobuf"
   AfterTargets="Clean">
    <RemoveDir Directories="$(ProtobufOutputFolder)"/>
  </Target>
</Project>

Общие настройки здесь были вынесены в PropertyGroup, а списки входных и выходных файлов заполняет новый target ComputeProtobufInput. Попутно (чтобы продемонстрировать работу со списками выходных файлов) была добавлена генерация кода из схемы для интеграции с python. Запускаем и проверяем что все работает правильно

1>------ Rebuild All started: Project: protobuf_test, Configuration: Debug x64 ------
1>Compiling schema test.proto
1>Compiling schema test2.proto
1>pch.cpp
1>protobuf_test.cpp
1>test.pb.cc
1>test2.pb.cc
1>Generating Code...
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
1>Done building project "protobuf_test.vcxproj".
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

А что насчет CustomBuildStep?


Надо сказать что разработчики из Майкрософт вполне здраво оценивали что все вышеописанное, хм, несколько нетривиально и плохо задокументировано и попытались облегчить жизнь программистов введя специальный таргет CustomBuildStep. В рамках этой концепции мы должны были бы в настройках файла отметить что наши .props файлы относятся к типу Custom Build Step

Затем следовало бы указать необходимые шаги по сборке во вкладке Custom Build Step

В .vcxproj-е это выглядит примерно вот так

  <ItemDefinitionGroup>
    <CustomBuildStep>
      <Command>$(Protoc) --cpp_out=.\generated\%(FileName).pb.cc %(FullPath)</Command>
      <Message>Generate protobuf files</Message>
      <Outputs>.\generated\%(FileName).pb.cc</Outputs>
    </CustomBuildStep>
  </ItemDefinitionGroup>
  <ItemGroup>
    ...
    <CustomBuild Include="test.proto"/>
    <CustomBuild Include="test2.proto"/>
   ...
  </ItemGroup>

Эта конструкция работает за счет того что введенные таким образом данные подставляются в недрах Microsoft.CppCommon.targets в специальный таргет CustomBuildStep который делает, в общем-то, все то же самое что я описал выше. Но работает все через GUI и не надо задумываться о реализации clean и tlog-ах :). При желании этот механизм вполне можно использовать, но я бы не рекомендовал этого делать в силу следующих соображений:

  • CustomBuildStep может быть только один на весь проект
    • Соответственно так обработать можно лишь 1 тип файлов на весь проект
    • Включать такой step в .props файл используемый для подключения ThirdParty библиотеки нецелесообразно, т.к. разные библиотеки могут его перекрывать друг у друга
  • Если в CustomBuildStep что-то ломается, то разобраться в том что случилось будет еще сложнее чем написать таргет с нуля

Правильное копирование файлов

Очень часто встречающейся разновидностью build target является копирование каких-нибудь файлов из одного места в другое. Например копирование файлов ресурсов в папку с собранным проектом или копирование thirdparty DLL к собранному бинарнику. И очень часто эту операцию реализуют «в лоб» через запуск консольной утилиты xcopy в Post-Build Targets. К примеру,

Так делать не надо по тем же самым причинам по которым не надо пытаться запихивать в Post-build steps другие build targets. Вместо этого мы можем напрямую указать Студии что ей необходимо скопировать тот или иной файл. К примеру если файл напрямую входит в проект, то ему достаточно указать ItemType=Copy

После нажатия кнопки apply появится дополнительная вкладка на которой можно настроить куда и как следует копировать выбранный файл. В коде .vcxproj-файла это будет выглядеть примерно так:

  <ItemGroup>
    ...
    <ProtobufSchema Include="test2.proto" />
    <CopyFileToFolders Include="resource.txt">
      <DestinationFolders>$(OutDir)</DestinationFolders>
    </CopyFileToFolders>
  </ItemGroup>

Всё заработает «из коробки», включая правильную поддержку tlog-файлов. Внутри это реализовано по все тому же принципу «специальной стандартной таски для копирования файлов» что и Custom Build Step которую я критиковал буквально в предыдущем разделе, но поскольку копирование файлов — довольно тривиальная операция и мы не переопределяем саму операцию (копирование) а лишь меняем список входных и выходных файлов для нее то работает это неплохо.

Замечу что при формировании списков файлов CopyFilesToFolder можно использовать wildcards. К примеру

<CopyFileToFolders Include="$(LibFolder)\*.dll">
      <DestinationFolders>$(OutDir)</DestinationFolders>
</CopyFileToFolders>

Добавление файлов в список CopyFileToFolders — пожалуй самый простой способ реализовать копирование при сборке проекта, в том числе в .props-файлах подключающих thirdparty-библиотеки. Однако если хочется получить больше контроля над происходящим, то еще одним вариантом является добавление в свои build target специализированной таски Copy. К примеру
<Target Name="_CopyLog4cppDll"
          Inputs="$(Log4cppDll)"
          Outputs="$(Log4cppDllTarget)"
          AfterTargets="PrepareForBuild">

    <Message Text="Copying log4cpp.dll..." importance="high"/>  
    <Copy SourceFiles="$(Log4cppDll)"
          DestinationFiles="$(Log4cppDllTarget)"
          SkipUnchangedFiles="true"
          Retries="10"
          RetryDelayMilliseconds="500" />
</Target>

Небольшое лирическое отступление

Вообще набор разнообразных стандартных task-ов у MS весьма обширен и включает в себя такие таски как DownloadFile, VerifyFileHash, Unzip и многие другие полезные примитивы. А стандартная таска Copy умеет делать Retry, пропускать не менявшиеся файлы и создавать hard-link вместо тупого копирования если это поддерживается файловой системой.


R сожалению таска Copy не поддерживает wildcards и не заполняет .tlog файлы. При желании это можно реализовать вручную,
к примеру так
  <Target Name="_PrepareToCopy">
    <ItemGroup>
      <MyFilesToCopy Include="$(LibFolder)\*.dll"/>
      <MyFilesToCopy>
        <DestinationFile>$(TargetFolder)\%(MyFilesToCopy.Filename)%(MyFilesToCopy.Extension)</DestinationFile>
      </MyFilesToCopy>
    </ItemGroup>
  </Target>
  <Target Name="_Copy" 
          Inputs="@(MyFilesToCopy)" 
          Outputs="%(MyFilesToCopy.DestinationFile)" 
          DependsOnTargets="_PrepareToCopy" 
          AfterTargets="PrepareForBuild">
    <Message Text="Copying %(MyFilesToCopy.Filename)..." importance="high" />
    <Copy SourceFiles="@(MyFilesToCopy)" 
          DestinationFolder="$(TargetFolder)" 
          SkipUnchangedFiles="true" 
          Retries="10" RetryDelayMilliseconds="500" />
     <WriteLinesToFile
       File="$(TLogLocation)\mycopy.read.1.tlog"
       Lines="^%(MyFilesToCopy.Identity)" />
     <WriteLinesToFile
       File="$(TLogLocation)\mycopy.write.1.tlog"
       Lines="^%(MyFilesToCopy.Identity);%(MyFilesToCopy.DestinationFile)" />
  </Target>

Однако работа с стандартным CopyFileToFolders обычно будет намного проще.

Level 3: интегрируемся с GUI от Visual Studio

Все то чем мы до сих пор занимались со стороны может показаться довольно унылой попыткой реализовать в не слишком подходящем для этого инструменте функциональность нормального make. Ручная правка XML-файлов, неочевидные конструкции для решения простых задач, костыльные tlog-файлы… Однако у билд-системы Студии есть и плюсы — к примеру после первоначальной настройки она обеспечивает получившимуся билд-плану неплохой графический интерфейс. Для его реализации используется тэг PropertyPageSchema о котором мы сейчас и поговорим

Вытаскиваем настройки из недр .vcxproj в Configuration Properties

Давайте попробуем сделать так чтобы мы могли бы редактировать свойство $(ProtobufOutputFolder) из «причесанной реализации protobuf.targets» не вручную в файле, а с комфортом прямо из IDE. Для этого нам потребуется написать специальный XAML-файл с описанием настроек. Открываем текстовый редактор и создаем файл с названием, к примеру, custom_settings.xml

<?xml version="1.0" encoding="utf-8"?>
<ProjectSchemaDefinitions xmlns="clr-namespace:Microsoft.Build.Framework.XamlTypes;assembly=Microsoft.Build.Framework">
  <Rule Name="CustomProperties" PageTemplate="generic" DisplayName="My own properties">
    <Rule.DataSource>
      <DataSource Persistence="ProjectFile"/>
    </Rule.DataSource> 
    <StringProperty Name="ProtobufOutputFolder"
                    DisplayName="Protobuf Output Directory"
                    Description="Directory where Protobuf generated files are created."
                    Subtype="folder">
    </StringProperty>
  </Rule>
</ProjectSchemaDefinitions>

Помимо собственно тэга StringProperty который указывает Студии на существование настройки «ProtobufOutputFolder» с типом String и Subtype=Folder и объясняет то как ее следует показывать в GUI, данный XML-ник указывает что хранить эту информацию следует в project file. Помимо ProjectFile можно использовать еще UserFile — тогда данные будут записаны в отдельный файлик .vcxproj.user который по задумке создателей Студии предназначается для приватных (не сохраняемых в VCS) настроек. Подключаем описанную нами схему к проекту, дописав в наш protobuf.targets тэг PropertyPageSchema

<ItemGroup>  
    <AvailableItemName Include="ProtobufSchema">
      <Targets>GenerateProtobuf</Targets>
    </AvailableItemName>
    <PropertyPageSchema Include="custom_settings.xml"/>
  </ItemGroup>

Для того чтобы наши правки вступили в силу перезапускаем Студию, загружаем наш проект, открываем project properties и видим…

Да! Появилась наша страничка с нашей настройкой и ее значение по умолчанию было верно прочитано Студией. Пробуем ее изменить, сохраняем проект, смотрим .vcxproj…

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <ProtobufOutputFolder>.\generated_custom</ProtobufOutputFolder>
  </PropertyGroup>

Как можно видеть по традиционному условию Condition, по умолчанию настройки ассоциированы с конкретной конфигурацией билда. Но при желании это можно перекрыть с помощью установки флага DataSource HasConfigurationCondition=«false». Правда в 2017 студии присутствует баг из-за которого настройки проекта могут не показываться если среди них нет хотя бы одной настройки ассоциированной с какой-то конфигурацией. К счастью эта настройка может быть невидимой.

Вариант без привязки к configuration
<?xml version="1.0" encoding="utf-8"?>
<Rule.DataSource>
/>
</Rule.DataSource>
<StringProperty Name="ProtobufOutputFolder"
DisplayName="Protobuf Output Directory"
Description="Directory where Protobuf generated files are created."
Subtype="folder">
<StringProperty.DataSource>
/>
</StringProperty.DataSource>

/>


Настроек можно добавлять сколько угодно. Возможные типы включают BoolProperty, StringProperty (с опциональными подтипами «folder» и «file»), StringListProperty, IntProperty, EnumProperty и DynamicEnumProperty причем последний может заполняться на лету из любого списка доступного в .vcxproj. Подробнее об этом можно почитать здесь. Можно так же группировать настройки в разделы. Попробуем к примеру добавить еще одну настройку типа Bool

Код
<?xml version="1.0" encoding="utf-8"?>
<ProjectSchemaDefinitions xmlns="clr-namespace:Microsoft.Build.Framework.XamlTypes;assembly=Microsoft.Build.Framework">
  <Rule Name="CustomProperties" PageTemplate="generic" DisplayName="My own properties">
    <Rule.DataSource>
      <DataSource Persistence="ProjectFile"/>
    </Rule.DataSource> 
    <Rule.Categories>
      <Category Name="General" DisplayName="General"/>
    </Rule.Categories>
    <BoolProperty Name="EnableCommonPCH" Category="General" DisplayName="Enable common precompiled headers" 
                    Description="Should we use solution-wide precompiled headers instead of project-specific?">
      <BoolProperty.DataSource>
        <DataSource HasConfigurationCondition="false" />
      </BoolProperty.DataSource>
    </BoolProperty>
    <StringProperty Name="ProtobufOutputFolder"
                    DisplayName="Protobuf Output Directory"
                    Description="Directory where Protobuf generated files are created."
                    Subtype="folder"
                    Category="General">
      <StringProperty.DataSource>
        <DataSource HasConfigurationCondition="false" />
      </StringProperty.DataSource>
    </StringProperty>
    <StringProperty Name="Dummy" Visible="false" />
  </Rule>
</ProjectSchemaDefinitions>

Перезапускаем Студию

Редактируем настройку, сохраняем проект — все работает как ожидалось

<PropertyGroup>
    <EnableCommonPCH>true</EnableCommonPCH>
  </PropertyGroup>
  <PropertyGroup>
    <ProtobufOutputFolder>.\generated_сustom</ProtobufOutputFolder>
  </PropertyGroup>

Объясняем Студии про новые типы файлов

До сих пор чтобы добавить в проект protobuf-файл нам необходимо было вручную прописывать в .vcxproj что это . Это легко исправить дописав к упомянутому выше .xml три тэга

  <ContentType
    Name="Protobuf"
    DisplayName="Google Protobuf Schema"
    ItemType="ProtobufSchema" />
  <ItemType
    Name="ProtobufSchema"
    DisplayName="Google Protobuf Schema" />
  <FileExtension
    Name="*.proto"
    ContentType="Protobuf" />

Перезапускаем студию, смотрим свойства у наших .proto файлов

Как легко видеть файлы теперь верно распознаются как «Google Protobuf Schema». К сожалению соответствующий пункт не добавляется автоматически в диалог «Add new item», но если мы добавим в проект уже существующий .proto-файл (контекстное меню проекта \ Add \ Existing item… ) то он распознается и добавится правильно. Кроме того наш новый «тип файлов» можно будет выбрать в выпадающем списке Item type:

Ассоциируем настройки с индивидуальными файлами

Помимо настроек «для проекта в целом» совершенно аналогичным образом можно сделать «настройки для отдельного файла». Достаточно указать в тэге DataSource аттрибут ItemType.

  <Rule Name="ProtobufProperties" PageTemplate="generic" DisplayName="Protobuf properties">
    <Rule.DataSource>
      <DataSource Persistence="ProjectFile" ItemType="ProtobufSchema" />
    </Rule.DataSource> 
    <Rule.Categories>
      <Category Name="General" DisplayName="General"/>
    </Rule.Categories>
    <StringProperty Name="dllexport_decl"
                    DisplayName="dllexport macro"
                    Description="Add dllexport / dllimport statements controlled by #define with this name."
                    Category="General">
    </StringProperty>
  </Rule>

Проверяем

Сохраняем, смотрим содержимое .vcxproj

    <ProtobufSchema Include="test2.proto">
      <dllexport_decl Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MYLIB_EXPORT</dllexport_decl>
    </ProtobufSchema>

Все работает как ожидалось.

Level 4: расширяем функциональность MSBuild

У меня никогда не возникало необходимости залезать в процесс сборки настолько глубоко, но раз уж статья так и так получилась немаленькой, то коротенько упомяну о последней возможности для кастомизации: расширение самого MSBuild. Помимо довольно обширной коллекции «стандартных» тасков, в MSBuild таски можно «импортировать» из разных источников с помощью тэга UsingTask. К примеру мы можем написать свое расширение для MSBuild, скомпилировать его в DLL-библиотеку и импортировать как-то вот так:


  <UsingTask 
    TaskName="CL"
    AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll"
  />


Именно так реализовано большинство «стандартных» тасков предоставляемых Студией. Но таскать с собою кастомную DLL для сборки по очевидным причинам частенько неудобно. Поэтому в тэге UsingTask поддерживается штука которая называется TaskFactory. TaskFactory можно считать «компилятором для task-ов» — мы передаем ей на вход некий исходный «мета-код», а она по нему генерирует реализующий его объект типа Task. К примеру с помощью CodeTaskFactory можно воткнуть код написанной на C# таски прямо внутрь .props-файла.
Подобный подход использует, к примеру Qt VS Tools

  <UsingTask TaskName="GetItemHash"
    TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Item               ParameterType="Microsoft.Build.Framework.ITaskItem" Required="true" />
      <Keys               ParameterType="System.String[]"                     Required="true" />
      <Hash Output="true" ParameterType="System.String" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.Text"/>
      <Using Namespace="System.IO"/>
      <Using Namespace="System.IO.Compression"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            var data = Encoding.UTF8.GetBytes(string.Concat(Keys.OrderBy(x => x)
                .Select(x => string.Format("[{0}={1}]", x, Item.GetMetadata(x))))
                .ToUpper());
            using (var dataZipped = new MemoryStream()) {
                using (var zip = new DeflateStream(dataZipped, CompressionLevel.Fastest))
                    zip.Write(data, 0, data.Length);
                Hash = Convert.ToBase64String(dataZipped.ToArray());
            }
        ]]>
      </Code>
    </Task>
  </UsingTask>

Если кто-то подобной функциональностью пользовался — отпишитесь об интересных use-case в комментариях.

На этом всё. Надеюсь что мне удалось показать как при настройке MSBuild работу с крупным проектом в Visual Studio можно сделать простой и удобной. Если Вы соберетесь внедрять у себя что-то из описанного выше, то дам небольшой совет: для отладки .props, .targets и .vcxproj удобно выставить MSBuild «отладочный» уровень логгирования в котором он весьма подробно пошагово расписывает свои действия с входными и выходными файлами

Спасибо всем кто дочитал до конца, надеюсь что получилось интересно :).

Делитесь своими рецептами для msbuild в комментариях — я постараюсь обновлять пост чтобы он служил исчерпывающим гайдом по конфигурированию solution в Студии.

Let's block ads! (Why?)